"""
Acceptance tests for Battery Tracker.

Each test gets a fresh in-memory SQLite database via the fixtures in conftest.py.
The `seeded_client` fixture pre-populates:
  Devices:   Device A (2 slots), Device B (1 slot)
  Batteries: BrandX 001 (id=1, available), BrandY 001 (id=2, available),
             BrandX 002 (id=3, retired)
"""

import pytest


# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #

def get_location(resp):
    """Return the redirect Location header (without host)."""
    loc = resp.headers.get("Location", "")
    if loc.startswith("http"):
        from urllib.parse import urlparse
        loc = urlparse(loc).path
    return loc


def follow(client, resp):
    """Follow a single redirect."""
    return client.get(get_location(resp))


# ------------------------------------------------------------------ #
# Home page
# ------------------------------------------------------------------ #

def test_home_loads(seeded_client):
    resp = seeded_client.get("/")
    assert resp.status_code == 200
    assert b"Available" in resp.data
    assert b"Installed" in resp.data
    assert b"Devices" in resp.data
    assert b"BrandX 001" not in resp.data  # no battery table on home page


def test_home_needs_attention_shows_low_capacity(client):
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/edit-details", data={"capacity_mah": "2000"})
    client.post("/battery/1/capacity-test/add",
                data={"tested_capacity_mah": "1500", "tested_date": "2026-01-01"})
    resp = client.get("/")
    assert b"Needs Attention" in resp.data
    assert b"Eneloop 001" in resp.data


def test_home_shows_device_counts(seeded_client):
    resp = seeded_client.get("/")
    assert resp.status_code == 200
    assert b"Devices" in resp.data


# ------------------------------------------------------------------ #
# Battery list
# ------------------------------------------------------------------ #

def test_battery_list_loads(seeded_client):
    resp = seeded_client.get("/battery/")
    assert resp.status_code == 200
    assert b"BrandX 001" in resp.data
    assert b"BrandY 001" in resp.data
    assert b"BrandX 002" in resp.data


def test_battery_list_has_filter_bar(seeded_client):
    resp = seeded_client.get("/battery/")
    assert b"filter-status" in resp.data
    assert b"filter-brand" in resp.data


# ------------------------------------------------------------------ #
# Battery add — label preview data
# ------------------------------------------------------------------ #

def test_battery_add_get_renders(client):
    resp = client.get("/battery/add")
    assert resp.status_code == 200
    assert b"prefixMaxNums" in resp.data


def test_battery_add_brand_counts_reflects_existing(seeded_client):
    # seeded_client has BrandX x2, BrandY x1
    resp = seeded_client.get("/battery/add")
    assert resp.status_code == 200
    assert b'"BrandX": 2' in resp.data or b'"BrandX":2' in resp.data
    assert b'"BrandY": 1' in resp.data or b'"BrandY":1' in resp.data


# ------------------------------------------------------------------ #
# Battery — bulk add
# ------------------------------------------------------------------ #

def test_bulk_add_batteries(client):
    resp = client.post("/battery/add",
                       data={"brand": "Eneloop", "count": "3"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Eneloop 001" in resp.data
    assert b"Eneloop 002" in resp.data
    assert b"Eneloop 003" in resp.data


def test_bulk_add_autogenerates_sequential_labels(client):
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    resp = client.post("/battery/add", data={"brand": "Eneloop", "count": "2"},
                       follow_redirects=True)
    assert b"Eneloop 003" in resp.data
    assert b"Eneloop 004" in resp.data


def test_bulk_add_missing_brand(client):
    resp = client.post("/battery/add", data={"brand": "", "count": "1"})
    assert resp.status_code == 400
    assert b"required" in resp.data.lower()


# ------------------------------------------------------------------ #
# Battery — detail
# ------------------------------------------------------------------ #

def test_battery_detail(seeded_client):
    resp = seeded_client.get("/battery/1")
    assert resp.status_code == 200
    assert b"BrandX 001" in resp.data
    assert b"BrandX" in resp.data


def test_battery_detail_not_found(client):
    resp = client.get("/battery/9999")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# Battery — edit notes
# ------------------------------------------------------------------ #

def test_edit_notes(seeded_client):
    seeded_client.post("/battery/1/edit-details", data={"notes": "test note here"})
    resp = seeded_client.get("/battery/1")
    assert b"test note here" in resp.data


# ------------------------------------------------------------------ #
# Battery — assign (per-battery, kept for special cases)
# ------------------------------------------------------------------ #

def test_edit_details_percentage_out_of_range_rejected(seeded_client):
    for bad in ("150", "-5"):
        resp = seeded_client.post("/battery/1/edit-details",
                                  data={"battery_percentage": bad},
                                  follow_redirects=True)
        assert b"between 0 and 100" in resp.data
    data = _json.loads(seeded_client.get("/export/all.json").data)
    bat = next(b for b in data["batteries"] if b["id"] == 1)
    assert bat["battery_percentage"] is None
    assert data["pct_logs"] == []


def test_edit_details_percentage_valid(seeded_client):
    seeded_client.post("/battery/1/edit-details", data={"battery_percentage": "100"})
    data = _json.loads(seeded_client.get("/export/all.json").data)
    bat = next(b for b in data["batteries"] if b["id"] == 1)
    assert bat["battery_percentage"] == 100
    assert any(l["battery_id"] == 1 and l["source"] == "manual" for l in data["pct_logs"])


def test_assign_battery(seeded_client):
    resp = seeded_client.post("/battery/1/assign",
                              data={"device_id": "1"},
                              follow_redirects=True)
    assert resp.status_code == 200
    assert b"Device A" in resp.data


def test_assign_retired_battery_blocked(seeded_client):
    # id=3 is retired
    resp = seeded_client.post("/battery/3/assign",
                              data={"device_id": "1"},
                              follow_redirects=True)
    assert resp.status_code == 200
    assert b"retired" in resp.data.lower()


def test_assign_over_capacity_blocked(seeded_client):
    # Fill Device B (1 slot) with id=1
    seeded_client.post("/battery/1/assign", data={"device_id": "2"})
    # Try to assign id=2 to the same full device
    resp = seeded_client.post("/battery/2/assign",
                              data={"device_id": "2"},
                              follow_redirects=True)
    assert resp.status_code == 200
    assert b"full" in resp.data.lower()


def test_brand_mix_warning(seeded_client):
    # Assign BrandX 001 (id=1) to Device A
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    # Assign BrandY 001 (id=2) to same Device A → brand mix warning, but succeeds
    resp = seeded_client.post("/battery/2/assign",
                              data={"device_id": "1"},
                              follow_redirects=True)
    assert resp.status_code == 200
    assert b"mixing" in resp.data.lower() or b"mix" in resp.data.lower() or b"brand" in resp.data.lower()


# ------------------------------------------------------------------ #
# Battery — unassign
# ------------------------------------------------------------------ #

def test_unassign_battery(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    resp = seeded_client.post("/battery/1/unassign", follow_redirects=True)
    assert resp.status_code == 200
    resp2 = seeded_client.get("/battery/1")
    assert b"available" in resp2.data.lower()


def test_unassign_next_honors_local_path(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    resp = seeded_client.post("/battery/1/unassign", data={"next": "/device/1"})
    assert get_location(resp) == "/device/1"


def test_unassign_next_rejects_external_redirect(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    resp = seeded_client.post("/battery/1/unassign", data={"next": "//evil.com/phish"})
    assert resp.headers["Location"] == "/battery/"


def test_unassign_all_next_rejects_external_redirect(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    resp = seeded_client.post("/device/1/unassign-all", data={"next": "//evil.com"})
    assert resp.headers["Location"] == "/device/"


# ------------------------------------------------------------------ #
# Battery — retire
# ------------------------------------------------------------------ #

def test_retire_battery(seeded_client):
    resp = seeded_client.post("/battery/1/retire", follow_redirects=True)
    assert resp.status_code == 200
    resp2 = seeded_client.get("/battery/1")
    assert b"retired" in resp2.data.lower()


def test_retire_clears_device(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    seeded_client.post("/battery/1/retire")
    resp = seeded_client.get("/battery/1")
    assert b"None" in resp.data or b"retired" in resp.data.lower()


def test_retire_already_retired(seeded_client):
    # id=3 is already retired by fixture
    resp = seeded_client.post("/battery/3/retire", follow_redirects=True)
    assert resp.status_code == 200
    assert b"already retired" in resp.data.lower()


# ------------------------------------------------------------------ #
# Battery — delete
# ------------------------------------------------------------------ #

def test_delete_battery_confirmation_page(seeded_client):
    resp = seeded_client.get("/battery/1/delete")
    assert resp.status_code == 200
    assert b"Confirm Delete" in resp.data
    assert b"BrandX 001" in resp.data


def test_delete_battery(seeded_client):
    seeded_client.post("/battery/1/delete")
    resp = seeded_client.get("/battery/1")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# Battery — bulk actions
# ------------------------------------------------------------------ #

def test_bulk_retire(client):
    client.post("/battery/add", data={"brand": "TestBrand", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "retire"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"retired" in client.get("/battery/1").data.lower()
    assert b"retired" in client.get("/battery/2").data.lower()


def test_bulk_delete(client):
    client.post("/battery/add", data={"brand": "TestBrand", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "delete"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert client.get("/battery/1").status_code == 404
    assert client.get("/battery/2").status_code == 404


def test_bulk_unassign(client):
    client.post("/device/add", data={"name": "Gadget", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "TestBrand", "count": "2"})
    client.post("/device/1/install", data={"brand[]": "TestBrand", "qty[]": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "unassign"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"available" in client.get("/battery/1").data.lower()


def test_bulk_set_brand(client):
    client.post("/battery/add", data={"brand": "OldBrand", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "set_brand",
                             "new_brand": "NewBrand"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"NewBrand" in client.get("/battery/1").data


def test_bulk_action_no_selection(client):
    client.post("/battery/add", data={"brand": "TestBrand", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"action": "retire"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"No batteries selected" in resp.data


# ------------------------------------------------------------------ #
# Device — install batteries
# ------------------------------------------------------------------ #

def test_device_install_autoselects(client):
    client.post("/device/add", data={"name": "Gadget", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "3"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "Eneloop", "qty[]": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    # Device detail should show 2 installed batteries
    assert b"Eneloop" in resp.data
    assert b"2 / 2" in resp.data


def test_device_install_mixed_brands(client):
    client.post("/device/add", data={"name": "Remote", "battery_slots": "4", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    client.post("/battery/add", data={"brand": "Energizer", "count": "2"})
    resp = client.post("/device/1/install",
                       data={"brand[]": ["Eneloop", "Energizer"], "qty[]": ["2", "2"]},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Eneloop" in resp.data
    assert b"Energizer" in resp.data


def test_device_install_insufficient_batteries(client):
    client.post("/device/add", data={"name": "Gadget", "battery_slots": "4", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "Eneloop", "qty[]": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"available" in resp.data.lower()


def test_device_install_over_capacity(client):
    client.post("/device/add", data={"name": "Gadget", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "3"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "Eneloop", "qty[]": "3"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"slot" in resp.data.lower()


def test_bulk_install_device(client):
    """Select multiple available batteries and install them into a device."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "3", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "install_device",
                             "device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Installed 2" in resp.data


def test_bulk_install_device_over_capacity(client):
    """Bulk install is blocked when device lacks free slots."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "install_device",
                             "device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"slot" in resp.data.lower()


def test_bulk_install_device_moves_installed_battery(client):
    """Bulk install moves a battery already installed in another device."""
    client.post("/device/add", data={"name": "Box A", "battery_slots": "2", "battery_size": "AA"})
    client.post("/device/add", data={"name": "Box B", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "install_device",
                             "device_id": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Installed 1" in resp.data
    assert b"Box B" in client.get("/battery/1").data


# ------------------------------------------------------------------ #
# Dashboard — quick-assign
# ------------------------------------------------------------------ #

def test_dashboard_quick_assign(client):
    """Quick-assign from dashboard (battery_assign POST) succeeds."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    resp = client.post("/battery/1/assign", data={"device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Box" in resp.data


def test_dashboard_quick_assign_full_device_blocked(client):
    """Quick-assign from dashboard to a full device is blocked."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    client.post("/battery/1/assign", data={"device_id": "1"})  # fills the slot
    resp = client.post("/battery/2/assign", data={"device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"full" in resp.data.lower()


def test_quick_assign_reassign_battery(client):
    """Quick-assign moves a battery that is already installed in another device."""
    client.post("/device/add", data={"name": "Box A", "battery_slots": "2", "battery_size": "AA"})
    client.post("/device/add", data={"name": "Box B", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})  # install into Box A
    resp = client.post("/battery/1/assign", data={"device_id": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Box B" in resp.data


def test_quick_assign_retired_battery_blocked(client):
    """battery_assign POST for a retired battery is blocked."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/retire")
    resp = client.post("/battery/1/assign", data={"device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"retired" in resp.data.lower()


# ------------------------------------------------------------------ #
# Device — install-one (specific battery)
# ------------------------------------------------------------------ #

def test_install_one_specific_battery(client):
    """device_install_one installs a chosen battery."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    resp = client.post("/device/1/install-one", data={"battery_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Eneloop 001" in resp.data


def test_install_one_over_capacity_blocked(client):
    """device_install_one to a full device is blocked."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
    client.post("/device/1/install-one", data={"battery_id": "1"})  # fills slot
    resp = client.post("/device/1/install-one", data={"battery_id": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"full" in resp.data.lower()


# ------------------------------------------------------------------ #
# Device — list
# ------------------------------------------------------------------ #

def test_device_list(seeded_client):
    resp = seeded_client.get("/device/")
    assert resp.status_code == 200
    assert b"Device A" in resp.data
    assert b"Device B" in resp.data


# ------------------------------------------------------------------ #
# Device — add
# ------------------------------------------------------------------ #

def test_add_device(client):
    resp = client.post("/device/add",
                       data={"name": "My Gadget", "battery_slots": "3", "battery_size": "AA"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"My Gadget" in resp.data


def test_add_device_duplicate_name(seeded_client):
    resp = seeded_client.post("/device/add",
                              data={"name": "Device A", "battery_slots": "2", "battery_size": "AA"})
    assert resp.status_code == 400
    assert b"already exists" in resp.data


def test_add_device_missing_name(client):
    resp = client.post("/device/add", data={"name": "", "battery_slots": "1"})
    assert resp.status_code == 400


# ------------------------------------------------------------------ #
# Device — detail
# ------------------------------------------------------------------ #

def test_device_detail(seeded_client):
    resp = seeded_client.get("/device/1")
    assert resp.status_code == 200
    assert b"Device A" in resp.data
    assert b"2" in resp.data  # battery_slots


def test_device_detail_not_found(client):
    resp = client.get("/device/9999")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# Device — delete
# ------------------------------------------------------------------ #

def test_delete_device_frees_batteries(seeded_client):
    seeded_client.post("/battery/1/assign", data={"device_id": "1"})
    resp = seeded_client.post("/device/1/delete", follow_redirects=True)
    assert resp.status_code == 200
    resp2 = seeded_client.get("/battery/1")
    assert b"available" in resp2.data.lower()


def test_delete_device_removed(seeded_client):
    seeded_client.post("/device/1/delete")
    resp = seeded_client.get("/device/1")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# Full round-trip
# ------------------------------------------------------------------ #

def test_add_device_with_type(client):
    resp = client.post("/device/add",
                       data={"name": "TV Remote", "battery_slots": "2", "battery_size": "AA",
                             "device_type": "Remote Control"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"TV Remote" in resp.data


def test_device_detail_shows_type(client):
    client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "battery_size": "AA", "device_type": "Flashlight"})
    resp = client.get("/device/1")
    assert resp.status_code == 200
    assert b"Flashlight" in resp.data


def test_edit_device_type(client):
    client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Flashlight" in resp.data


def test_add_device_with_location(client):
    resp = client.post("/device/add",
                       data={"name": "Bedroom Remote", "battery_slots": "2", "battery_size": "AA",
                             "location": "Bedroom"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Bedroom Remote" in resp.data
    resp = client.get("/device/1")
    assert b"Bedroom" in resp.data


def test_edit_device_location(client):
    client.post("/device/add", data={"name": "Living Room Clock", "battery_slots": "1", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Living Room Clock", "battery_slots": "1", "location": "Living Room"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Living Room" in resp.data


def test_add_capacity_test(client):
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    resp = client.post("/battery/1/capacity-test/add",
                       data={"tested_capacity_mah": "1900", "tested_date": "2026-01-01"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"1900" in resp.data


def test_capacity_test_syncs_battery(client):
    """Adding a test updates battery.tested_capacity_mah."""
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/capacity-test/add",
                data={"tested_capacity_mah": "1800", "tested_date": "2026-01-01"})
    resp = client.get("/battery/1")
    assert b"1800" in resp.data


def test_delete_capacity_test(client):
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/capacity-test/add",
                data={"tested_capacity_mah": "1900", "tested_date": "2026-01-01"})
    resp = client.post("/battery/1/capacity-test/1/delete", follow_redirects=True)
    assert resp.status_code == 200
    assert b"No test records" in resp.data


def test_add_install_delete_battery(client):
    client.post("/device/add", data={"name": "Gadget", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "AcmeBrand", "count": "1"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "AcmeBrand", "qty[]": "1"},
                       follow_redirects=True)
    assert b"Gadget" in resp.data
    client.post("/battery/1/delete")
    assert client.get("/battery/1").status_code == 404


# ------------------------------------------------------------------ #
# Device — battery_size
# ------------------------------------------------------------------ #


def test_add_device_with_battery_size(client):
    """Device with battery_size shows it on detail page."""
    resp = client.post("/device/add",
                       data={"name": "AA Remote", "battery_slots": "2", "battery_size": "AA"},
                       follow_redirects=True)
    assert resp.status_code == 200
    resp2 = client.get("/device/1")
    assert b"AA" in resp2.data


def test_install_one_filters_by_size(client):
    """Single-battery install dropdown excludes batteries with wrong size."""
    client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})    # id=1
    client.post("/battery/1/edit-details", data={"size": "AA"})
    client.post("/battery/add", data={"brand": "Energizer", "count": "1"})  # id=2
    client.post("/battery/2/edit-details", data={"size": "AAA"})
    resp = client.get("/device/1")
    assert resp.status_code == 200
    # AA battery should appear in the dropdown, AAA should not
    assert b"Eneloop 001" in resp.data
    assert b"Energizer 001" not in resp.data


def test_bulk_install_filters_by_size(client):
    """Bulk install with a brand that only has wrong-size batteries fails with 0 available."""
    client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Energizer", "count": "2"})
    # Set both batteries to AAA (explicitly incompatible)
    client.post("/battery/1/edit-details", data={"size": "AAA"})
    client.post("/battery/2/edit-details", data={"size": "AAA"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "Energizer", "qty[]": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# Import
# ------------------------------------------------------------------ #

import io
import json as _json


def _make_import_payload(devices=None, batteries=None,
                         charge_logs=None, capacity_tests=None, pct_logs=None):
    data = {
        "exported_at": "2026-01-01T00:00:00",
        "devices":        devices        or [],
        "batteries":      batteries      or [],
        "charge_logs":    charge_logs    or [],
        "capacity_tests": capacity_tests or [],
        "pct_logs":       pct_logs       or [],
    }
    return (io.BytesIO(_json.dumps(data).encode()), "export.json")


def _post_import(client, payload_tuple):
    buf, fname = payload_tuple
    return client.post(
        "/import",
        data={"file": (buf, fname)},
        content_type="multipart/form-data",
    )


def _bat(id, label, brand="Brand", status="available", device_id=None, **kw):
    return {"id": id, "label": label, "brand": brand, "status": status,
            "device_id": device_id, "size": None, "chemistry": None, "capacity_mah": None,
            "tested_capacity_mah": None, "tested_date": None, "charge_cycles": None,
            "purchase_date": None, "storage_location": None, "battery_percentage": None,
            "notes": None, **kw}


def _dev(id, name, battery_slots=2, battery_size="AA", **kw):
    return {"id": id, "name": name, "battery_slots": battery_slots,
            "battery_size": battery_size, "device_type": None,
            "location": None, "ha_entity_id": None, "notes": None, **kw}


def test_import_page_loads(client):
    resp = client.get("/import")
    assert resp.status_code == 200
    assert b"Import" in resp.data
    assert b'enctype="multipart/form-data"' in resp.data


def test_import_no_file_returns_400(client):
    resp = client.post("/import", data={}, content_type="multipart/form-data")
    assert resp.status_code == 400


def test_import_invalid_json_returns_400(client):
    buf = io.BytesIO(b"this is not json")
    resp = client.post("/import",
                       data={"file": (buf, "bad.json")},
                       content_type="multipart/form-data")
    assert resp.status_code == 400
    assert b"Invalid JSON" in resp.data


def test_import_missing_required_keys_returns_400(client):
    buf = io.BytesIO(_json.dumps({"exported_at": "2026-01-01"}).encode())
    resp = client.post("/import",
                       data={"file": (buf, "bad.json")},
                       content_type="multipart/form-data")
    assert resp.status_code == 400


def test_import_creates_devices_and_batteries(client):
    payload = _make_import_payload(
        devices=[_dev(1, "RC Car")],
        batteries=[_bat(1, "Eneloop 001", brand="Eneloop")],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    assert b"Import Results" in resp.data
    dash = client.get("/battery/")
    assert b"Eneloop 001" in dash.data
    assert b"RC Car" in dash.data


def test_import_results_show_correct_counts(client):
    payload = _make_import_payload(
        devices=[_dev(10, "DevX")],
        batteries=[_bat(10, "BrandX 001"), _bat(11, "BrandX 002")],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    # devices_created=1, batteries_created=2 appear in the results table
    assert b"Import Results" in resp.data


def test_import_charge_logs_remapped_correctly(client):
    payload = _make_import_payload(
        batteries=[_bat(99, "Eneloop 001", brand="Eneloop")],
        charge_logs=[{"id": 1, "battery_id": 99, "battery_label": "Eneloop 001",
                      "charged_date": "2026-01-15", "increment_cycles": 1, "notes": None}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"2026-01-15" in detail.data


def test_import_capacity_tests_remapped(client):
    payload = _make_import_payload(
        batteries=[_bat(5, "Test 001", capacity_mah=2000)],
        capacity_tests=[{"id": 1, "battery_id": 5, "battery_label": "Test 001",
                         "tested_capacity_mah": 1850, "tested_date": "2026-02-01", "notes": None}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"1850" in detail.data


def test_import_skips_existing_device(client):
    client.post("/device/add", data={"name": "RC Car", "battery_slots": "2", "battery_size": "AA"})
    payload = _make_import_payload(
        devices=[_dev(1, "RC Car", battery_slots=4)],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    # Original 2-slot device not overwritten
    detail = client.get("/device/1")
    assert b"RC Car" in detail.data


def test_import_skips_existing_battery(client):
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    payload = _make_import_payload(
        batteries=[_bat(999, "Eneloop 001", brand="Eneloop", status="retired")],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"available" in detail.data.lower()


def test_import_log_skipped_when_battery_not_in_map(client):
    payload = _make_import_payload(
        batteries=[],
        charge_logs=[{"id": 1, "battery_id": 42, "battery_label": "Ghost",
                      "charged_date": "2026-01-01", "increment_cycles": 1, "notes": None}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    assert b"Import Results" in resp.data


def test_import_battery_gets_installed_when_device_maps(client):
    payload = _make_import_payload(
        devices=[_dev(7, "My Device")],
        batteries=[_bat(7, "Linked 001", status="installed", device_id=7)],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"installed" in detail.data.lower()
    assert b"My Device" in detail.data


def test_import_battery_retired_preserved_when_no_device(client):
    payload = _make_import_payload(
        batteries=[_bat(3, "Old 001", status="retired")],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"retired" in detail.data.lower()


def test_import_battery_device_id_unknown_becomes_available(client):
    payload = _make_import_payload(
        devices=[],
        batteries=[_bat(5, "Orphan 001", status="installed", device_id=99)],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert b"available" in detail.data.lower()


def test_full_roundtrip_export_import(client):
    client.post("/device/add", data={"name": "Router", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})

    export_resp = client.get("/export/all.json")
    assert export_resp.status_code == 200

    buf = io.BytesIO(export_resp.data)
    resp = client.post("/import",
                       data={"file": (buf, "export.json")},
                       content_type="multipart/form-data")
    assert resp.status_code == 200
    assert b"Import Results" in resp.data


def test_export_json_includes_parent_fields(client):
    _setup_rc_car(client)
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["RC Car Set"]["parent_id"] is None
    assert devs["Remote"]["parent_id"] == devs["RC Car Set"]["id"]
    assert devs["Remote"]["parent_name"] == "RC Car Set"


def test_import_rebuilds_hierarchy(client):
    payload = _make_import_payload(devices=[
        _dev(10, "Hub", battery_slots=0, battery_size=None),
        _dev(11, "Probe", parent_id=10),
    ])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["Probe"]["parent_id"] == devs["Hub"]["id"]
    assert devs["Probe"]["parent_name"] == "Hub"
    # parent's 0 slots survive the import (previously coerced to 1)
    assert devs["Hub"]["battery_slots"] == 0


def test_import_child_listed_before_parent(client):
    payload = _make_import_payload(devices=[
        _dev(11, "Probe", parent_id=10),
        _dev(10, "Hub", battery_slots=0, battery_size=None),
    ])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["Probe"]["parent_id"] == devs["Hub"]["id"]


def test_import_subcomponent_missing_parent_becomes_top_level(client):
    payload = _make_import_payload(devices=[_dev(11, "Probe", parent_id=99)])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["Probe"]["parent_id"] is None


def test_import_empty_battery_size_stored_as_null(client):
    payload = _make_import_payload(devices=[_dev(10, "NoSize", battery_size="")])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["NoSize"]["battery_size"] is None


def test_roundtrip_import_preserves_hierarchy(client):
    _setup_rc_car(client)
    data = _json.loads(client.get("/export/all.json").data)
    # rename everything so the import creates rows instead of skipping
    for d in data["devices"]:
        d["name"] = d["name"] + " v2"
    buf = io.BytesIO(_json.dumps(data).encode())
    resp = client.post("/import", data={"file": (buf, "export.json")},
                       content_type="multipart/form-data")
    assert resp.status_code == 200
    data2 = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data2["devices"]}
    assert devs["Remote v2"]["parent_id"] == devs["RC Car Set v2"]["id"]
    assert devs["Car v2"]["parent_id"] == devs["RC Car Set v2"]["id"]


def test_device_detail_unassign_all(seeded_client):
    client = seeded_client
    # install battery 1 into device 1 (2-slot AA device)
    client.post("/device/1/install-one", data={"battery_id": "1"})
    # unassign all from device detail, redirecting back to device detail
    resp = client.post(
        "/device/1/unassign-all",
        data={"next": "/device/1"},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"Unassigned" in resp.data
    # device detail should now show 0 installed
    assert b"0 / 2" in resp.data


def test_device_install_batch(seeded_client):
    client = seeded_client
    # batteries 1 (BrandX AA) and 2 (BrandY AA) are available; device 1 has 2 slots
    resp = client.post(
        "/device/1/install-batch",
        data={"battery_ids": ["1", "2"]},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"Installed 2" in resp.data
    assert b"2 / 2" in resp.data


def test_device_charge_all(seeded_client):
    client = seeded_client
    # install battery 1 into device 1
    client.post("/device/1/install-one", data={"battery_id": "1"})
    resp = client.post(
        "/device/1/charge-all",
        data={"charged_date": "2025-01-01", "increment_cycles": "1"},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"Logged charge for 1 battery" in resp.data
    assert b"+cycles" in resp.data


def test_device_charge_all_no_installed(seeded_client):
    client = seeded_client
    # device 1 has no batteries installed yet
    resp = client.post(
        "/device/1/charge-all",
        data={"charged_date": "2025-01-01"},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"No installed batteries" in resp.data


# ------------------------------------------------------------------ #
# Sub-components (self-referential device hierarchy)
# ------------------------------------------------------------------ #

def _setup_rc_car(client):
    """Create RC Car Set with Remote and Car sub-components and 6 AA batteries."""
    client.post("/device/add", data={"name": "RC Car Set", "battery_slots": "0", "battery_size": ""})
    # id=1 above; add sub-components with parent_id=1
    client.post("/device/add", data={"name": "Remote", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/device/add", data={"name": "Car", "battery_slots": "4",
                                     "battery_size": "AA", "parent_id": "1"})
    for i in range(1, 7):
        client.post("/battery/add", data={"brand": "Eneloop", "label": f"E{i:03d}", "size": "AA"})


def test_add_subcomponent(client):
    _setup_rc_car(client)
    # Parent detail shows sub-components
    resp = client.get("/device/1")
    assert resp.status_code == 200
    assert b"Remote" in resp.data
    assert b"Car" in resp.data
    # Child detail shows breadcrumb back to parent
    resp = client.get("/device/2")
    assert resp.status_code == 200
    assert b"RC Car Set" in resp.data
    assert b"Remote" in resp.data


def test_device_with_children_cannot_become_subcomponent(client):
    _setup_rc_car(client)
    # Garage is parent-eligible (top-level, 0 slots, no size)
    client.post("/device/add", data={"name": "Garage", "battery_slots": "0", "battery_size": ""})
    resp = client.post("/device/1/edit",
                       data={"name": "RC Car Set", "battery_slots": "0", "parent_id": "4"},
                       follow_redirects=True)
    assert b"cannot itself become a sub-component" in resp.data
    data = _json.loads(client.get("/export/all.json").data)
    devs = {d["name"]: d for d in data["devices"]}
    assert devs["RC Car Set"]["parent_id"] is None


def test_subcomponent_prevents_deep_nesting(client):
    _setup_rc_car(client)
    # id=2 is "Remote", which already has parent_id=1
    resp = client.post("/device/add", data={"name": "Button", "battery_slots": "1",
                                            "battery_size": "AA", "parent_id": "2"})
    assert resp.status_code == 400
    assert b"one level" in resp.data.lower() or b"nest" in resp.data.lower()


def test_subcomponent_install_batteries(client):
    _setup_rc_car(client)
    # Install 2 batteries into Remote (device id=2)
    resp = client.post("/device/2/install-one", data={"battery_id": "1"}, follow_redirects=True)
    assert resp.status_code == 200
    resp = client.post("/device/2/install-one", data={"battery_id": "2"}, follow_redirects=True)
    assert resp.status_code == 200
    # Remote is now full (2/2); a third battery should be blocked
    resp = client.post("/device/2/install-one", data={"battery_id": "3"}, follow_redirects=True)
    assert resp.status_code == 200
    assert b"full" in resp.data.lower()


def test_battery_list_shows_parent_slash_child(client):
    _setup_rc_car(client)
    client.post("/device/2/install-one", data={"battery_id": "1"})
    resp = client.get("/battery/", follow_redirects=True)
    assert resp.status_code == 200
    assert b"RC Car Set" in resp.data
    assert b"Remote" in resp.data


def test_delete_parent_promotes_children(client):
    _setup_rc_car(client)
    client.post("/device/1/delete", follow_redirects=True)
    # Child devices still exist
    resp = client.get("/device/2")
    assert resp.status_code == 200
    resp = client.get("/device/3")
    assert resp.status_code == 200


def test_device_without_parent_unchanged(client):
    client.post("/device/add", data={"name": "Torch", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
    resp = client.post("/device/1/install-one", data={"battery_id": "1"}, follow_redirects=True)
    assert resp.status_code == 200
    assert b"1 / 2" in resp.data


def test_subcomponent_ha_entity_id(client):
    client.post("/device/add", data={"name": "Hub", "battery_slots": "0", "battery_size": ""})
    resp = client.post(
        "/device/add",
        data={"name": "Sensor A", "battery_slots": "1", "battery_size": "AA",
              "parent_id": "1", "ha_entity_id": "sensor.sensor_a_battery"},
        follow_redirects=True,
    )
    # Redirects to parent detail on success
    assert resp.status_code == 200
    assert b"Sensor A" in resp.data
    # Child detail shows parent breadcrumb
    resp = client.get("/device/2")
    assert resp.status_code == 200
    assert b"Hub" in resp.data


# ------------------------------------------------------------------ #
# Sub-component hierarchy — parent_key, null type/location, list hiding
# ------------------------------------------------------------------ #

def _setup_parent_with_two_children(client):
    """Hub (top-level, Sensor type, Living Room) with Sensor A and Sensor B."""
    client.post("/device/add", data={
        "name": "Hub", "battery_slots": "0", "battery_size": "",
        "device_type": "Sensor", "location": "Living Room",
    }, follow_redirects=True)
    client.post("/device/add", data={
        "name": "Sensor A", "battery_slots": "1", "battery_size": "AA", "parent_id": "1",
    }, follow_redirects=True)
    client.post("/device/add", data={
        "name": "Sensor B", "battery_slots": "1", "battery_size": "AA", "parent_id": "1",
    }, follow_redirects=True)


def test_device_list_nests_subcomponents(client):
    _setup_parent_with_two_children(client)
    resp = client.get("/device/")
    assert resp.status_code == 200
    assert b"Hub" in resp.data
    # sub-components render as indented child rows under the parent
    assert b"Sensor A" in resp.data
    assert b"Sensor B" in resp.data
    assert "↳".encode() in resp.data
    # parent name is searchable on child rows
    assert b'data-name="hub sensor a"' in resp.data


def test_assign_page_shows_parent_prefix(client):
    _setup_rc_car(client)
    resp = client.get("/battery/1/assign")
    assert resp.status_code == 200
    assert b"RC Car Set /" in resp.data


def test_subcomponent_type_location_null(client):
    _setup_parent_with_two_children(client)
    # Sub-component detail shows inherited type/location from parent
    resp = client.get("/device/2")
    assert resp.status_code == 200
    assert b"Sensor" in resp.data
    assert b"Living Room" in resp.data
    assert b"inherited" in resp.data
    # Submitting type/location in edit for sub-component should have no effect
    client.post("/device/2/edit", data={
        "name": "Sensor A", "battery_slots": "1",
        "device_type": "Remote Control", "location": "Bedroom",
        "parent_id": "1",  # keep sub-component relationship
    })
    resp2 = client.get("/device/2")
    assert b"Remote Control" not in resp2.data
    assert b"Bedroom" not in resp2.data


def test_subcomponent_name_unique_per_parent(client):
    _setup_parent_with_two_children(client)

    # Duplicate top-level name → 400
    resp = client.post("/device/add", data={
        "name": "Hub", "battery_slots": "0", "battery_size": "",
    })
    assert resp.status_code == 400
    assert b"already exists" in resp.data

    # Duplicate child name within same parent → 400
    resp = client.post("/device/add", data={
        "name": "Sensor A", "battery_slots": "1", "battery_size": "AA", "parent_id": "1",
    })
    assert resp.status_code == 400
    assert b"already exists" in resp.data

    # Same child name under a DIFFERENT parent → success
    client.post("/device/add", data={
        "name": "Hub 2", "battery_slots": "0", "battery_size": "",
    })  # id=4
    resp = client.post("/device/add", data={
        "name": "Sensor A", "battery_slots": "1", "battery_size": "AA", "parent_id": "4",
    }, follow_redirects=True)
    assert resp.status_code == 200


def test_parent_battery_summary_flat(client):
    _setup_parent_with_two_children(client)
    client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})  # ids 1, 2
    client.post("/device/2/install-one", data={"battery_id": "1"})
    client.post("/device/3/install-one", data={"battery_id": "2"})

    resp = client.get("/device/1")
    assert resp.status_code == 200
    # Flat table has Component column heading
    assert b"Component" in resp.data
    # Both batteries appear
    assert b"Eneloop 001" in resp.data
    assert b"Eneloop 002" in resp.data
    # Both child names linked in the component column
    assert b"Sensor A" in resp.data
    assert b"Sensor B" in resp.data
    # Flat table heading appears once (not duplicated per sub-component)
    assert resp.data.count(b"<h2>Installed Batteries</h2>") == 1


def test_device_charge_all_parent(client):
    _setup_parent_with_two_children(client)
    client.post("/battery/add", data={"brand": "Eneloop", "label": "E001", "size": "AA"})
    client.post("/battery/add", data={"brand": "Eneloop", "label": "E002", "size": "AA"})
    client.post("/device/2/install-one", data={"battery_id": "1"})
    client.post("/device/3/install-one", data={"battery_id": "2"})
    resp = client.post(
        "/device/1/charge-all",
        data={"charged_date": "2025-06-01", "increment_cycles": "1"},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"Logged charge for 2 batteri" in resp.data
    assert b"+cycles" in resp.data


def test_device_charge_all_parent_no_installed(client):
    _setup_parent_with_two_children(client)
    resp = client.post(
        "/device/1/charge-all",
        data={"charged_date": "2025-06-01"},
        follow_redirects=True,
    )
    assert resp.status_code == 200
    assert b"No installed batteries" in resp.data


# ------------------------------------------------------------------ #
# Battery — unretire
# ------------------------------------------------------------------ #

def test_unretire_battery(seeded_client):
    # id=3 is retired by fixture
    resp = seeded_client.post("/battery/3/unretire", follow_redirects=True)
    assert resp.status_code == 200
    assert b"now available again" in resp.data
    assert b"available" in seeded_client.get("/battery/3").data.lower()


def test_unretire_not_retired(seeded_client):
    resp = seeded_client.post("/battery/1/unretire", follow_redirects=True)
    assert b"is not retired" in resp.data


# ------------------------------------------------------------------ #
# Logbook
# ------------------------------------------------------------------ #

def test_battery_logbook_add_and_delete(seeded_client):
    resp = seeded_client.post("/battery/1/logbook/add",
                              data={"body": "Swapped terminals"}, follow_redirects=True)
    assert b"Swapped terminals" in resp.data
    resp = seeded_client.post("/battery/1/logbook/1/delete", follow_redirects=True)
    assert b"Logbook entry deleted" in resp.data
    assert b"Swapped terminals" not in seeded_client.get("/battery/1").data


def test_battery_logbook_add_empty_body(seeded_client):
    resp = seeded_client.post("/battery/1/logbook/add",
                              data={"body": "  "}, follow_redirects=True)
    assert b"Entry text is required" in resp.data


def test_device_logbook_add_and_delete(seeded_client):
    resp = seeded_client.post("/device/1/logbook/add",
                              data={"body": "Cleaned contacts"}, follow_redirects=True)
    assert b"Cleaned contacts" in resp.data
    resp = seeded_client.post("/device/1/logbook/1/delete", follow_redirects=True)
    assert b"Logbook entry deleted" in resp.data
    assert b"Cleaned contacts" not in seeded_client.get("/device/1").data


def test_device_logbook_add_empty_body(seeded_client):
    resp = seeded_client.post("/device/1/logbook/add",
                              data={"body": ""}, follow_redirects=True)
    assert b"Entry text is required" in resp.data


# ------------------------------------------------------------------ #
# Charge log — delete
# ------------------------------------------------------------------ #

def test_charge_log_delete_reverts_cycles(seeded_client):
    seeded_client.post("/battery/1/charge-log/add",
                       data={"charged_date": "2026-06-01", "increment_cycles": "1"})
    data = _json.loads(seeded_client.get("/export/all.json").data)
    assert next(b for b in data["batteries"] if b["id"] == 1)["charge_cycles"] == 1
    log_id = data["charge_logs"][0]["id"]
    resp = seeded_client.post(f"/battery/1/charge-log/{log_id}/delete",
                              follow_redirects=True)
    assert b"Charge log entry deleted" in resp.data
    data = _json.loads(seeded_client.get("/export/all.json").data)
    assert next(b for b in data["batteries"] if b["id"] == 1)["charge_cycles"] == 0
    assert data["charge_logs"] == []


# ------------------------------------------------------------------ #
# CSV exports
# ------------------------------------------------------------------ #

def test_csv_export_routes(seeded_client):
    expectations = {
        "/export/batteries.csv": b"label",
        "/export/devices.csv": b"parent_id,parent_name",
        "/export/charge-logs.csv": b"charged_date",
        "/export/capacity-tests.csv": b"tested_capacity_mah",
        "/export/pct-logs.csv": b"percentage",
    }
    for url, marker in expectations.items():
        resp = seeded_client.get(url)
        assert resp.status_code == 200, url
        assert "text/csv" in resp.content_type, url
        assert marker in resp.data, url
    resp = seeded_client.get("/export/csv.zip")
    assert resp.status_code == 200
    assert "zip" in resp.content_type


# ------------------------------------------------------------------ #
# Sub-component edit — type/location stay NULL
# ------------------------------------------------------------------ #

def test_subcomponent_edit_resave_keeps_nulls(client):
    _setup_rc_car(client)
    # device 2 = Remote, sub-component of 1; re-save smuggling in type/location
    resp = client.post("/device/2/edit", data={
        "name": "Remote", "battery_slots": "2", "parent_id": "1",
        "device_type": "Toy", "location": "Garage", "battery_size": "AA",
    }, follow_redirects=True)
    assert resp.status_code == 200
    data = _json.loads(client.get("/export/all.json").data)
    rem = next(d for d in data["devices"] if d["name"] == "Remote")
    assert rem["parent_id"] is not None
    assert rem["device_type"] is None
    assert rem["location"] is None
