"""
Tests targeting uncovered lines from coverage analysis.
Covers: 404 paths, validation errors, edge cases, model reprs,
ha_client/ha_poller branches, export/import edge cases, bulk actions.
"""

import io
import json
import pytest
from unittest.mock import patch, MagicMock


# ------------------------------------------------------------------ #
# Helpers (mirrors test_acceptance.py)
# ------------------------------------------------------------------ #

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


def follow(client, resp):
    return client.get(get_location(resp))


def _make_json(devices=None, batteries=None, charge_logs=None,
               capacity_tests=None, pct_logs=None):
    return json.dumps({
        "exported_at": "2026-01-01",
        "devices":        devices        or [],
        "batteries":      batteries      or [],
        "charge_logs":    charge_logs    or [],
        "capacity_tests": capacity_tests or [],
        "pct_logs":       pct_logs       or [],
    }).encode()


def _post_import(client, raw_bytes, filename="export.json"):
    return client.post(
        "/import",
        data={"file": (io.BytesIO(raw_bytes), filename)},
        content_type="multipart/form-data",
    )


# ------------------------------------------------------------------ #
# _parse_date helper (lines 18, 22-23)
# ------------------------------------------------------------------ #

def test_parse_date_valid(client):
    """Valid date passes through battery_add without error."""
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post(
        "/battery/1/edit-details",
        data={"purchase_date": "2026-01-15"},
        follow_redirects=True,
    )
    assert resp.status_code == 200


def test_parse_date_invalid_returns_none(client):
    """Invalid date string is treated as None (no crash, details updated)."""
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post(
        "/battery/1/edit-details",
        data={"purchase_date": "not-a-date"},
        follow_redirects=True,
    )
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# service_worker route (line 105)
# ------------------------------------------------------------------ #

def test_service_worker_served(client):
    resp = client.get("/sw.js")
    assert resp.status_code in (200, 404)  # 404 if file absent; route exists


# ------------------------------------------------------------------ #
# export page (line 1236)
# ------------------------------------------------------------------ #

def test_export_page_loads(client):
    resp = client.get("/export")
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# battery_add POST — count ValueError and _int ValueError (163-164, 177-178)
# ------------------------------------------------------------------ #

def test_battery_add_invalid_count_falls_back_to_one(client):
    """Non-integer count silently falls back to 1."""
    resp = client.post("/battery/add",
                       data={"brand": "Eneloop", "count": "abc"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Eneloop 001" in resp.data
    assert b"Eneloop 002" not in resp.data


def test_battery_add_invalid_capacity_ignored(client):
    """Non-integer capacity_mah in add form is stored as None (no crash)."""
    resp = client.post("/battery/add",
                       data={"brand": "Eneloop", "count": "1",
                             "capacity_mah": "not-a-number"},
                       follow_redirects=True)
    assert resp.status_code == 200
    detail = client.get("/battery/1")
    assert detail.status_code == 200


# ------------------------------------------------------------------ #
# battery_add GET — size-prefix numbering (lines 216, 221-223)
# ------------------------------------------------------------------ #

def test_battery_add_get_size_prefix_numbering(client):
    """GET /battery/add includes size-prefixed prefix_max_nums."""
    client.post("/battery/add",
                data={"brand": "Eneloop", "count": "2", "size": "AA",
                      "include_size_in_label": "on"})
    resp = client.get("/battery/add")
    assert resp.status_code == 200
    assert b"prefixMaxNums" in resp.data


# ------------------------------------------------------------------ #
# battery_edit_details — 404 and _int ValueError (284, 291-292)
# ------------------------------------------------------------------ #

def test_battery_edit_details_404(client):
    resp = client.post("/battery/9999/edit-details", data={"notes": "x"})
    assert resp.status_code == 404


def test_battery_edit_details_invalid_int_field(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/edit-details",
                       data={"capacity_mah": "xyz"},
                       follow_redirects=True)
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# battery_capacity_test_add — 404, missing fields, invalid mah (336-349)
# ------------------------------------------------------------------ #

def test_capacity_test_add_404(client):
    resp = client.post("/battery/9999/capacity-test/add",
                       data={"tested_capacity_mah": "1000", "tested_date": "2026-01-01"})
    assert resp.status_code == 404


def test_capacity_test_add_missing_date(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/capacity-test/add",
                       data={"tested_capacity_mah": "1000", "tested_date": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower() or b"valid date" in resp.data.lower()


def test_capacity_test_add_missing_mah(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/capacity-test/add",
                       data={"tested_capacity_mah": "", "tested_date": "2026-01-01"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower() or b"valid date" in resp.data.lower()


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


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


# ------------------------------------------------------------------ #
# battery_capacity_test_delete — 404 (363)
# ------------------------------------------------------------------ #

def test_capacity_test_delete_404(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/capacity-test/9999/delete")
    assert resp.status_code == 404


def test_capacity_test_delete_wrong_battery(client):
    """Delete attempt where test belongs to a different battery → 404."""
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    client.post("/battery/1/capacity-test/add",
                data={"tested_capacity_mah": "1000", "tested_date": "2026-01-01"})
    # test id=1 belongs to battery id=1; try deleting it via battery id=2
    resp = client.post("/battery/2/capacity-test/1/delete")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# battery_charge_log_add — 404 and missing date (379, 382-383)
# ------------------------------------------------------------------ #

def test_charge_log_add_404(client):
    resp = client.post("/battery/9999/charge-log/add",
                       data={"charged_date": "2026-01-01"})
    assert resp.status_code == 404


def test_charge_log_add_missing_date(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/charge-log/add",
                       data={"charged_date": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"valid date" in resp.data.lower()


# ------------------------------------------------------------------ #
# battery_charge_log_delete — 404 (396)
# ------------------------------------------------------------------ #

def test_charge_log_delete_404_battery(client):
    resp = client.post("/battery/9999/charge-log/1/delete")
    assert resp.status_code == 404


def test_charge_log_delete_wrong_battery(client):
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    client.post("/battery/1/charge-log/add",
                data={"charged_date": "2026-01-01"})
    # log id=1 belongs to battery 1; try deleting via battery 2
    resp = client.post("/battery/2/charge-log/1/delete")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# battery_assign — 404, device not found, device has children (412, 425-430)
# ------------------------------------------------------------------ #

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


def test_battery_assign_post_device_not_found(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/assign",
                       data={"device_id": "9999"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"not found" in resp.data.lower()


def test_battery_assign_to_device_with_children_blocked(client):
    """Assigning to a parent device (has sub-components) is blocked."""
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/assign",
                       data={"device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


# ------------------------------------------------------------------ #
# battery_unassign — 404 (469)
# ------------------------------------------------------------------ #

def test_battery_unassign_404(client):
    resp = client.post("/battery/9999/unassign")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# battery_retire — 404 (484)
# ------------------------------------------------------------------ #

def test_battery_retire_404(client):
    resp = client.post("/battery/9999/retire")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# battery_unretire — 404 (502)
# ------------------------------------------------------------------ #

def test_battery_unretire_404(client):
    resp = client.post("/battery/9999/unretire")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# battery_delete — 404 GET (519)
# ------------------------------------------------------------------ #

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


# ------------------------------------------------------------------ #
# bulk_action — set_brand no brand, install_device no device_id,
#   device not found, has_children, no eligible, brand mix,
#   already there + retired skipped notes (565-621)
# ------------------------------------------------------------------ #

def test_bulk_set_brand_empty_brand(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "set_brand", "new_brand": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower()


def test_bulk_install_device_no_device_id(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "install_device"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"select a device" in resp.data.lower()


def test_bulk_install_device_not_found(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "install_device",
                             "device_id": "9999"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"not found" in resp.data.lower()


def test_bulk_install_device_has_children_blocked(client):
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "install_device",
                             "device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


def test_bulk_install_no_eligible_batteries(client):
    """All selected batteries are retired → no eligible batteries."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/retire")
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "install_device",
                             "device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no eligible" in resp.data.lower()


def test_bulk_install_brand_mix_warning(client):
    """Bulk install produces brand-mix warning when mixing brands."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "4", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Alpha", "count": "1"})
    client.post("/battery/add", data={"brand": "Beta", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})  # install Alpha first
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["2"], "action": "install_device",
                             "device_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"mixing" in resp.data.lower() or b"brand" in resp.data.lower()


def test_bulk_install_already_there_note(client):
    """Battery already in target device → message notes 'already there'."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    client.post("/battery/1/assign", data={"device_id": "1"})  # already in Box
    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"already there" in resp.data.lower()


def test_bulk_install_retired_skipped_note(client):
    """Retired battery in selection → 'retired skipped' note in message."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    client.post("/battery/2/retire")
    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"retired skipped" in resp.data.lower()


# ------------------------------------------------------------------ #
# bulk_action — set_field, log_charged, unknown (624-655)
# ------------------------------------------------------------------ #

def test_bulk_set_field_storage_location(client):
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "set_field",
                             "field_name": "storage_location", "field_value": "Drawer"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"Drawer" in client.get("/battery/1").data


def test_bulk_set_field_invalid_field(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "set_field",
                             "field_name": "status", "field_value": "retired"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"invalid field" in resp.data.lower()


def test_bulk_set_field_brand_empty(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "set_field",
                             "field_name": "brand", "field_value": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower()


def test_bulk_log_charged(client):
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1", "2"], "action": "log_charged",
                             "charged_date": "2026-01-15"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"logged charge" in resp.data.lower()


def test_bulk_log_charged_with_cycles(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "log_charged",
                             "charged_date": "2026-01-15", "increment_cycles": "on"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"+cycles" in resp.data.lower() or b"cycles" in resp.data.lower()


def test_bulk_log_charged_missing_date(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "log_charged",
                             "charged_date": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"valid date" in resp.data.lower()


def test_bulk_unknown_action(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/bulk-action",
                       data={"battery_ids": ["1"], "action": "bogus_action"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"unknown action" in resp.data.lower()


# ------------------------------------------------------------------ #
# ha_entities — when HA not enabled (line 666)
# ------------------------------------------------------------------ #

def test_ha_entities_disabled(client):
    resp = client.get("/ha/entities")
    assert resp.status_code == 200
    assert resp.json == []


# ------------------------------------------------------------------ #
# device_add GET (line 782)
# ------------------------------------------------------------------ #

def test_device_add_get(client):
    resp = client.get("/device/add")
    assert resp.status_code == 200


def test_device_add_get_with_invalid_parent_id(client):
    """parent_id that is not a digit → prefill_parent remains None, no crash."""
    resp = client.get("/device/add?parent_id=abc")
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# device_add POST — parent validation (710-727, 743-746)
# ------------------------------------------------------------------ #

def test_device_add_parent_not_found(client):
    resp = client.post("/device/add",
                       data={"name": "Child", "battery_slots": "2",
                             "battery_size": "AA", "parent_id": "9999"})
    assert resp.status_code == 400
    assert b"not found" in resp.data.lower()


def test_device_add_parent_is_subcomponent_blocked(client):
    """Cannot nest sub-components more than one level."""
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    resp = client.post("/device/add",
                       data={"name": "GrandChild", "battery_slots": "1",
                             "battery_size": "AA", "parent_id": "2"})
    assert resp.status_code == 400
    assert b"one level" in resp.data.lower()


def test_device_add_parent_has_slots_blocked(client):
    """Parent must have battery_slots=0."""
    client.post("/device/add", data={"name": "NotAParent", "battery_slots": "2",
                                     "battery_size": "AA"})
    resp = client.post("/device/add",
                       data={"name": "Sub", "battery_slots": "1",
                             "battery_size": "AA", "parent_id": "1"})
    assert resp.status_code == 400


def test_device_add_invalid_slots(client):
    resp = client.post("/device/add",
                       data={"name": "Box", "battery_slots": "-1", "battery_size": "AA"})
    assert resp.status_code == 400
    assert b"non-negative" in resp.data.lower()


def test_device_add_invalid_slots_text(client):
    resp = client.post("/device/add",
                       data={"name": "Box", "battery_slots": "abc", "battery_size": "AA"})
    assert resp.status_code == 400


# ------------------------------------------------------------------ #
# device_edit — validation errors (858-910)
# ------------------------------------------------------------------ #

def test_device_edit_404(client):
    resp = client.post("/device/9999/edit", data={"name": "X", "battery_slots": "1"})
    assert resp.status_code == 404


def test_device_edit_empty_name(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "", "battery_slots": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower()


def test_device_edit_invalid_slots_negative(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Box", "battery_slots": "-1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"positive" in resp.data.lower()


def test_device_edit_zero_slots_without_children(client):
    """Setting slots to 0 on a device without children is invalid."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Box", "battery_slots": "0"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"positive" in resp.data.lower()


def test_device_edit_duplicate_name(client):
    client.post("/device/add", data={"name": "Box A", "battery_slots": "2", "battery_size": "AA"})
    client.post("/device/add", data={"name": "Box B", "battery_slots": "1", "battery_size": "AA"})
    resp = client.post("/device/2/edit",
                       data={"name": "Box A", "battery_slots": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"already exists" in resp.data.lower()


def test_device_edit_parent_not_found(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Box", "battery_slots": "2", "parent_id": "9999"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"not found" in resp.data.lower()


def test_device_edit_own_parent(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/edit",
                       data={"name": "Box", "battery_slots": "2", "parent_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"own parent" in resp.data.lower()


def test_device_edit_cannot_make_parent_into_subcomponent(client):
    """A device that already has children cannot become a sub-component."""
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/device/add", data={"name": "Other", "battery_slots": "0", "battery_size": ""})
    resp = client.post("/device/1/edit",
                       data={"name": "Parent", "battery_slots": "0", "parent_id": "3"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


def test_device_edit_new_parent_is_subcomponent(client):
    """Cannot use a sub-component as a parent (no double nesting)."""
    client.post("/device/add", data={"name": "P", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "C", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/device/add", data={"name": "X", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/3/edit",
                       data={"name": "X", "battery_slots": "2", "parent_id": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"one level" in resp.data.lower()


def test_device_edit_new_parent_has_slots(client):
    """New parent must have battery_slots=0."""
    client.post("/device/add", data={"name": "NotParent", "battery_slots": "2", "battery_size": "AA"})
    client.post("/device/add", data={"name": "Child", "battery_slots": "1", "battery_size": "AA"})
    resp = client.post("/device/2/edit",
                       data={"name": "Child", "battery_slots": "1", "parent_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"battery slots set to 0" in resp.data.lower()


# ------------------------------------------------------------------ #
# device_install — 404 and has_children and no pairs (946-967)
# ------------------------------------------------------------------ #

def test_device_install_404(client):
    resp = client.post("/device/9999/install",
                       data={"brand[]": "B", "qty[]": "1"})
    assert resp.status_code == 404


def test_device_install_has_children_blocked(client):
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "B", "qty[]": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


def test_device_install_no_pairs(client):
    """Submit form with empty brand/qty → 'No batteries specified'."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "", "qty[]": "0"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no batteries specified" in resp.data.lower()


def test_device_install_invalid_qty_falls_to_zero(client):
    """Non-integer qty treated as 0 → no pairs → 'No batteries specified'."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    resp = client.post("/device/1/install",
                       data={"brand[]": "B", "qty[]": "abc"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no batteries specified" in resp.data.lower()


# ------------------------------------------------------------------ #
# device_install_one — 404, has_children, battery not found,
#   brand mix warning, size mismatch warning (1021-1045)
# ------------------------------------------------------------------ #

def test_device_install_one_404(client):
    resp = client.post("/device/9999/install-one", data={"battery_id": "1"})
    assert resp.status_code == 404


def test_device_install_one_has_children_blocked(client):
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/device/1/install-one",
                       data={"battery_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


def test_device_install_one_battery_not_found(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/install-one",
                       data={"battery_id": "9999"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"not found" in resp.data.lower() or b"not available" in resp.data.lower()


def test_device_install_one_brand_mix_warning(client):
    """Installing a different brand produces a warning flash."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "3", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "Alpha", "count": "1"})
    client.post("/battery/add", data={"brand": "Beta", "count": "1"})
    client.post("/device/1/install-one", data={"battery_id": "1"})
    resp = client.post("/device/1/install-one",
                       data={"battery_id": "2"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"mixing" in resp.data.lower() or b"brand" in resp.data.lower()


def test_device_install_one_size_mismatch_warning(client):
    """Installing wrong-size battery produces a warning flash."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/edit-details", data={"size": "AAA"})
    resp = client.post("/device/1/install-one",
                       data={"battery_id": "1"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"aaa" in resp.data.lower() or b"size" in resp.data.lower()


# ------------------------------------------------------------------ #
# device_delete — 404 (1064)
# ------------------------------------------------------------------ #

def test_device_delete_404(client):
    resp = client.post("/device/9999/delete")
    assert resp.status_code == 404


def test_device_delete_with_children_detaches_them(client):
    """Deleting a parent frees sub-components as independent devices."""
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    resp = client.post("/device/1/delete", follow_redirects=True)
    assert resp.status_code == 200
    assert b"independent" in resp.data.lower()


# ------------------------------------------------------------------ #
# device_unassign_all — 404 (1088)
# ------------------------------------------------------------------ #

def test_device_unassign_all_404(client):
    resp = client.post("/device/9999/unassign-all")
    assert resp.status_code == 404


# ------------------------------------------------------------------ #
# device_install_batch — 404, has_children, no batteries,
#   overflow, none installed (1110-1136)
# ------------------------------------------------------------------ #

def test_device_install_batch_404(client):
    resp = client.post("/device/9999/install-batch",
                       data={"battery_ids": ["1"]})
    assert resp.status_code == 404


def test_device_install_batch_has_children_blocked(client):
    client.post("/device/add", data={"name": "Parent", "battery_slots": "0", "battery_size": ""})
    client.post("/device/add", data={"name": "Child", "battery_slots": "2",
                                     "battery_size": "AA", "parent_id": "1"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/device/1/install-batch",
                       data={"battery_ids": ["1"]},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"sub-component" in resp.data.lower()


def test_device_install_batch_no_batteries_selected(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/install-batch",
                       data={},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no batteries selected" in resp.data.lower()


def test_device_install_batch_none_installed_warning(client):
    """Selecting an already-installed battery → none actually installed."""
    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": "B", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})
    resp = client.post("/device/2/install-batch",
                       data={"battery_ids": ["1"]},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no batteries were installed" in resp.data.lower()


def test_device_install_batch_respects_free_slots(client):
    """Batch stops once free slots are exhausted (line 1122)."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "3"})
    resp = client.post("/device/1/install-batch",
                       data={"battery_ids": ["1", "2", "3"]},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"1 / 1" in resp.data


# ------------------------------------------------------------------ #
# device_charge_all — 404 and missing date (1147-1151)
# ------------------------------------------------------------------ #

def test_device_charge_all_404(client):
    resp = client.post("/device/9999/charge-all",
                       data={"charged_date": "2026-01-01"})
    assert resp.status_code == 404


def test_device_charge_all_missing_date(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/assign", data={"device_id": "1"})
    resp = client.post("/device/1/charge-all",
                       data={"charged_date": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"valid date" in resp.data.lower()


def test_device_charge_all_no_installed_batteries(client):
    """charge-all on a device with no installed batteries → warning."""
    client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
    resp = client.post("/device/1/charge-all",
                       data={"charged_date": "2026-01-01"},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"no installed batteries" in resp.data.lower()


# ------------------------------------------------------------------ #
# CSV exports — charge_logs, capacity_tests, pct_logs (1210, 1220, 1230)
# ------------------------------------------------------------------ #

def test_export_charge_logs_csv(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/charge-log/add", data={"charged_date": "2026-01-01"})
    resp = client.get("/export/charge-logs.csv")
    assert resp.status_code == 200
    assert b"charged_date" in resp.data
    assert b"2026-01-01" in resp.data


def test_export_capacity_tests_csv(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/capacity-test/add",
                data={"tested_capacity_mah": "1800", "tested_date": "2026-01-01"})
    resp = client.get("/export/capacity-tests.csv")
    assert resp.status_code == 200
    assert b"tested_capacity_mah" in resp.data
    assert b"1800" in resp.data


def test_export_pct_logs_csv(client):
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    client.post("/battery/1/edit-details", data={"battery_percentage": "75"})
    resp = client.get("/export/pct-logs.csv")
    assert resp.status_code == 200
    assert b"percentage" in resp.data
    assert b"75" in resp.data


# ------------------------------------------------------------------ #
# Import edge cases (1347-1511)
# ------------------------------------------------------------------ #

def test_import_file_too_large(client):
    big = b"x" * (10 * 1024 * 1024 + 1)
    resp = _post_import(client, big)
    assert resp.status_code == 400
    assert b"too large" in resp.data.lower()


def test_import_missing_required_keys(client):
    payload = json.dumps({"exported_at": "2026-01-01"}).encode()
    resp = _post_import(client, payload)
    assert resp.status_code == 400


def test_import_battery_empty_label_skipped(client):
    payload = _make_json(batteries=[
        {"id": 1, "label": "", "brand": "B", "status": "available",
         "device_id": None, "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},
    ])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    assert client.get("/battery/1").status_code == 404


def test_import_capacity_test_unresolved_battery_skipped(client):
    """capacity_test referencing a battery not in the payload is skipped."""
    payload = _make_json(
        batteries=[],
        capacity_tests=[{"id": 1, "battery_id": 999, "battery_label": "X",
                         "tested_capacity_mah": 1800, "tested_date": "2026-01-01",
                         "notes": None}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200


def test_import_pct_log_unresolved_battery_skipped(client):
    """pct_log referencing a battery not in the payload is skipped."""
    payload = _make_json(
        batteries=[],
        pct_logs=[{"id": 1, "battery_id": 999, "battery_label": "X",
                   "percentage": 80, "recorded_at": "2026-01-01 00:00:00",
                   "source": "manual"}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200


def test_import_pct_logs_appended(client):
    """pct_logs for an imported battery are appended."""
    payload = _make_json(
        batteries=[{"id": 5, "label": "Foo 001", "brand": "Foo", "status": "available",
                    "device_id": None, "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}],
        pct_logs=[{"id": 1, "battery_id": 5, "battery_label": "Foo 001",
                   "percentage": 80, "recorded_at": "2026-01-01 00:00:00",
                   "source": "manual"}],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    csv_resp = client.get("/export/pct-logs.csv")
    assert b"80" in csv_resp.data


def test_import_device_with_subcomponent(client):
    """Import correctly links sub-components to their parent's new ID."""
    payload = _make_json(
        devices=[
            {"id": 10, "name": "Hub", "battery_slots": 0, "battery_size": None,
             "device_type": None, "location": None, "ha_entity_id": None,
             "notes": None, "parent_id": None},
            {"id": 11, "name": "Module", "battery_slots": 2, "battery_size": "AA",
             "device_type": None, "location": None, "ha_entity_id": None,
             "notes": None, "parent_id": 10},
        ],
    )
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    device_list = client.get("/device/")
    assert b"Hub" in device_list.data


# ------------------------------------------------------------------ #
# Logbook — 404 paths (1539, 1555, 1567, 1583)
# ------------------------------------------------------------------ #

def test_battery_logbook_add_404(client):
    resp = client.post("/battery/9999/logbook/add", data={"body": "test"})
    assert resp.status_code == 404


def test_battery_logbook_delete_404(client):
    resp = client.post("/battery/9999/logbook/1/delete")
    assert resp.status_code == 404


def test_device_logbook_add_404(client):
    resp = client.post("/device/9999/logbook/add", data={"body": "test"})
    assert resp.status_code == 404


def test_device_logbook_delete_404(client):
    resp = client.post("/device/9999/logbook/1/delete")
    assert resp.status_code == 404


def test_battery_logbook_add_empty_body(client):
    """Empty logbook body is rejected with flash error (no 404)."""
    client.post("/battery/add", data={"brand": "B", "count": "1"})
    resp = client.post("/battery/1/logbook/add",
                       data={"body": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower()


def test_device_logbook_add_empty_body(client):
    client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
    resp = client.post("/device/1/logbook/add",
                       data={"body": ""},
                       follow_redirects=True)
    assert resp.status_code == 200
    assert b"required" in resp.data.lower()


def test_battery_logbook_delete_wrong_battery(client):
    """Logbook entry not belonging to the battery is silently ignored (no 404)."""
    client.post("/battery/add", data={"brand": "B", "count": "2"})
    client.post("/battery/1/logbook/add", data={"body": "Entry"})
    resp = client.post("/battery/2/logbook/1/delete", follow_redirects=True)
    assert resp.status_code == 200


def test_device_logbook_delete_wrong_device(client):
    client.post("/device/add", data={"name": "A", "battery_slots": "1", "battery_size": "AA"})
    client.post("/device/add", data={"name": "B", "battery_slots": "1", "battery_size": "AA"})
    client.post("/device/1/logbook/add", data={"body": "Entry"})
    resp = client.post("/device/2/logbook/1/delete", follow_redirects=True)
    assert resp.status_code == 200


# ------------------------------------------------------------------ #
# Model __repr__ methods (models.py lines 100, 157, 172, 187, 202, 217)
# ------------------------------------------------------------------ #

def test_model_reprs(app):
    from models import Battery, Device, CapacityTest, ChargeLog, BatteryPctLog, Logbook
    d = Device(name="TestDevice", battery_slots=2)
    assert "TestDevice" in repr(d)

    b = Battery(label="Test 001", brand="B", status="available")
    assert "Test 001" in repr(b)

    ct = CapacityTest(battery_id=1, tested_capacity_mah=1800, tested_date="2026-01-01")
    assert "1800" in repr(ct)

    cl = ChargeLog(battery_id=1, charged_date="2026-01-01", increment_cycles=0)
    assert "2026-01-01" in repr(cl)

    pl = BatteryPctLog(battery_id=1, percentage=80,
                       recorded_at="2026-01-01 00:00:00")
    assert "80" in repr(pl)

    lb = Logbook(body="note", recorded_at="2026-01-01 00:00:00")
    assert "2026-01-01" in repr(lb)


# ------------------------------------------------------------------ #
# ha_client — disabled path and error path (lines 49, 67-69)
# ------------------------------------------------------------------ #

def test_ha_client_list_battery_entities_disabled():
    from ha_client import HomeAssistantClient
    c = HomeAssistantClient(url=None, api_key=None)
    assert c.list_battery_entities() == []


def test_ha_client_list_battery_entities_error():
    from ha_client import HomeAssistantClient
    import requests as req_lib
    c = HomeAssistantClient(url="http://fake", api_key="key")
    with patch("ha_client.requests.get",
               side_effect=req_lib.ConnectionError("boom")):
        result = c.list_battery_entities()
    assert result == []


# ------------------------------------------------------------------ #
# ha_poller — stop() and exception path (lines 32, 64-66)
# ------------------------------------------------------------------ #

def test_ha_poller_stop():
    from ha_poller import HaPoller
    from ha_client import HomeAssistantClient
    client = HomeAssistantClient(url=None, api_key=None)
    poller = HaPoller(client, MagicMock(), interval=300)
    poller.stop()
    assert poller._stop.is_set()


def test_ha_poller_poll_once_exception_path(app):
    """_poll_once rolls back and logs on DB exception without crashing."""
    from ha_poller import HaPoller
    from ha_client import HomeAssistantClient

    ha = HomeAssistantClient(url="http://fake", api_key="key")

    broken_session = MagicMock()
    broken_session.query.side_effect = RuntimeError("db error")

    poller = HaPoller(ha, lambda: broken_session, interval=300)
    poller._poll_once()

    broken_session.rollback.assert_called_once()
    broken_session.close.assert_called_once()


# ------------------------------------------------------------------ #
# battery_add GET — non-numeric label (line 216)
# ------------------------------------------------------------------ #

def test_battery_add_get_non_numeric_label_skipped(client):
    """battery_add GET skips prefix numbering for labels without trailing digits."""
    payload = _make_json(batteries=[
        {"id": 1, "label": "Custom", "brand": "B", "status": "available",
         "device_id": None, "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},
    ])
    _post_import(client, payload)
    resp = client.get("/battery/add")
    assert resp.status_code == 200
    assert b"prefixMaxNums" in resp.data


# ------------------------------------------------------------------ #
# import — device with empty name is skipped (lines 1377-1378)
# ------------------------------------------------------------------ #

def test_import_device_empty_name_skipped(client):
    """Import skips devices whose name is empty."""
    payload = _make_json(devices=[
        {"id": 1, "name": "", "battery_slots": 2, "battery_size": "AA",
         "device_type": None, "location": None, "ha_entity_id": None,
         "notes": None, "parent_id": None},
    ])
    resp = _post_import(client, payload)
    assert resp.status_code == 200
    assert client.get("/device/9999").status_code == 404


# ------------------------------------------------------------------ #
# import — exception handler (lines 1508-1511)
# ------------------------------------------------------------------ #

def test_import_exception_handler(client):
    """Import with malformed batteries list triggers the 500 exception handler."""
    payload = json.dumps({
        "devices": [],
        "batteries": "not-a-list",
    }).encode()
    resp = _post_import(client, payload)
    assert resp.status_code == 500
    assert b"Import failed" in resp.data


# ------------------------------------------------------------------ #
# ha_poller _run loop (line 37)
# ------------------------------------------------------------------ #

def test_ha_poller_run_loop_executes_poll(app):
    """_run loop calls _poll_once at least once before being stopped."""
    from ha_poller import HaPoller
    from ha_client import HomeAssistantClient

    ha = HomeAssistantClient(url=None, api_key=None)
    call_count = [0]

    poller = HaPoller(ha, MagicMock(), interval=0.01)

    def fake_poll():
        call_count[0] += 1
        poller.stop()

    poller._poll_once = fake_poll
    poller._run()

    assert call_count[0] >= 1


# ------------------------------------------------------------------ #
# can_have_subcomponents model method
# ------------------------------------------------------------------ #

def test_can_have_subcomponents_true(app):
    from models import Device
    d = Device(name="Parent", battery_slots=0, parent_id=None, parent_key=-1)
    assert d.can_have_subcomponents() is True


def test_can_have_subcomponents_false_has_slots(app):
    from models import Device
    d = Device(name="Regular", battery_slots=2, parent_id=None, parent_key=-1)
    assert d.can_have_subcomponents() is False


def test_can_have_subcomponents_false_is_subcomponent(app):
    from models import Device
    d = Device(name="Child", battery_slots=0, parent_id=1, parent_key=1)
    assert d.can_have_subcomponents() is False
