#!/usr/bin/env python3

from __future__ import annotations

import importlib.machinery
import importlib.util
import json
import pathlib
import sys
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


ROOT = pathlib.Path(__file__).resolve().parents[2]
HELPER = ROOT / "config/dot/quickshell/scripts/panama-home-assistant"
QML_SERVICE = ROOT / "config/dot/quickshell/services/HomeAssistant.qml"

loader = importlib.machinery.SourceFileLoader("panama_home_assistant", str(HELPER))
spec = importlib.util.spec_from_loader(loader.name, loader)
bridge = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = bridge
loader.exec_module(bridge)


class FakeHomeAssistant(BaseHTTPRequestHandler):
    requests: list[dict[str, object]] = []

    def log_message(self, _format: str, *args: object) -> None:
        del args

    def _record(self, body: bytes = b"") -> None:
        self.requests.append(
            {
                "method": self.command,
                "path": self.path,
                "authorization": self.headers.get("Authorization"),
                "body": body,
            }
        )

    def _json(self, value: object, status: int = 200) -> None:
        payload = json.dumps(value).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def do_GET(self) -> None:
        self._record()
        if self.path == "/api/":
            self._json({"message": "API running."})
            return
        if self.path == "/api/states":
            self._json(
                [
                    {
                        "entity_id": "light.kitchen",
                        "state": "on",
                        "attributes": {
                            "friendly_name": "Kitchen",
                            "brightness": 128,
                            "supported_color_modes": ["brightness"],
                        },
                    },
                    {
                        "entity_id": "light.hall",
                        "state": "off",
                        "attributes": {
                            "friendly_name": "Hall",
                            "supported_color_modes": ["color_temp"],
                        },
                    },
                    {"entity_id": "sensor.private", "state": "1", "attributes": {"token": "never-return"}},
                    {
                        "entity_id": "light.malformed",
                        "state": "on",
                        "attributes": "invalid",
                    },
                    {
                        "entity_id": "light.corner",
                        "state": "unavailable",
                        "attributes": {
                            "friendly_name": "Corner",
                            "supported_color_modes": ["brightness"],
                        },
                    },
                ]
            )
            return
        self._json({"message": "not found"}, 404)

    def do_POST(self) -> None:
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length)
        self._record(body)
        if self.path in {
            "/api/services/homeassistant/toggle",
            "/api/services/light/turn_on",
            "/api/services/light/turn_off",
        }:
            self._json([])
            return
        self._json({"message": "not found"}, 404)


class HomeAssistantBridgeTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.server = ThreadingHTTPServer(("127.0.0.1", 0), FakeHomeAssistant)
        cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
        cls.thread.start()
        cls.base_url = f"http://127.0.0.1:{cls.server.server_port}"

    @classmethod
    def tearDownClass(cls) -> None:
        cls.server.shutdown()
        cls.server.server_close()
        cls.thread.join(timeout=2)

    def setUp(self) -> None:
        FakeHomeAssistant.requests.clear()

    def config(self) -> object:
        return bridge.Config(
            base_url=self.base_url,
            token="fixture-token",
            entity_ids=("light.kitchen", "light.hall"),
        )

    def test_environment_configuration_wins_over_legacy_sources(self) -> None:
        env = {
            "PANAMA_HOME_ASSISTANT_URL": "https://home.example",
            "PANAMA_HOME_ASSISTANT_TOKEN": "fixture-token",
            "PANAMA_HOME_ASSISTANT_ENTITIES": "light.kitchen,light.hall",
        }

        config = bridge.resolve_config(
            env,
            legacy=lambda: (_ for _ in ()).throw(AssertionError("legacy called")),
        )

        self.assertEqual(config.base_url, "https://home.example")
        self.assertEqual(config.entity_ids, ("light.kitchen", "light.hall"))

    def test_config_does_not_require_legacy_entity_ids(self) -> None:
        self.assertTrue(
            bridge.Config(
                base_url="https://home.example",
                token="fixture-token",
                entity_ids=(),
            ).configured
        )

    def test_legacy_entity_ids_remain_migration_metadata(self) -> None:
        config = bridge.resolve_config(
            {
                "PANAMA_HOME_ASSISTANT_URL": "https://home.example",
                "PANAMA_HOME_ASSISTANT_TOKEN": "fixture-token",
            },
            legacy=lambda: bridge.Config(
                base_url="https://legacy.example",
                token="legacy-token",
                entity_ids=("light.kitchen",),
            ),
        )

        self.assertEqual(config.base_url, "https://home.example")
        self.assertEqual(config.token, "fixture-token")
        self.assertEqual(config.entity_ids, ("light.kitchen",))

    def test_catalog_filters_and_normalizes_discovered_lights(self) -> None:
        catalog = bridge.collect_catalog(self.config())

        self.assertTrue(catalog["ok"])
        self.assertEqual(
            [item["sourceName"] for item in catalog["entities"]],
            ["Kitchen", "Hall", "Corner"],
        )
        self.assertEqual(catalog["entities"][0]["brightnessPct"], 50)
        self.assertEqual(catalog["entities"][1]["brightnessPct"], 0)
        self.assertTrue(all(item["dimmable"] for item in catalog["entities"]))
        rendered = json.dumps(catalog)
        self.assertNotIn("sensor.private", rendered)
        self.assertNotIn("light.malformed", rendered)
        self.assertNotIn("token", rendered)

    def test_snapshot_uses_bearer_authentication(self) -> None:
        bridge.collect_catalog(self.config())

        request = FakeHomeAssistant.requests[-1]
        self.assertEqual(request["method"], "GET")
        self.assertEqual(request["path"], "/api/states")
        self.assertEqual(request["authorization"], "Bearer fixture-token")

    def test_snapshot_remains_a_catalog_compatibility_alias(self) -> None:
        snapshot = bridge.collect_snapshot(self.config())

        self.assertTrue(snapshot["ok"])
        self.assertEqual(
            [item["sourceName"] for item in snapshot["entities"]],
            ["Kitchen", "Hall", "Corner"],
        )

    def test_action_rejects_an_entity_not_in_the_live_catalog(self) -> None:
        with self.assertRaisesRegex(ValueError, "entity-not-discovered"):
            bridge.toggle(self.config(), "light.office")

    def test_toggle_authorizes_against_discovered_catalog(self) -> None:
        result = bridge.toggle(self.config(), "light.corner")

        self.assertTrue(result["ok"])
        request = FakeHomeAssistant.requests[-1]
        self.assertEqual(request["method"], "POST")
        self.assertEqual(request["path"], "/api/services/homeassistant/toggle")
        self.assertEqual(
            json.loads(request["body"]),
            {"entity_id": "light.corner"},
        )

    def test_brightness_uses_turn_on_for_positive_percent(self) -> None:
        bridge.set_brightness(self.config(), "light.kitchen", 62)

        request = FakeHomeAssistant.requests[-1]
        self.assertEqual(request["path"], "/api/services/light/turn_on")
        self.assertEqual(
            json.loads(request["body"]),
            {"entity_id": "light.kitchen", "brightness_pct": 62},
        )

    def test_brightness_zero_uses_turn_off(self) -> None:
        bridge.set_brightness(self.config(), "light.hall", 0)

        request = FakeHomeAssistant.requests[-1]
        self.assertEqual(request["path"], "/api/services/light/turn_off")
        self.assertEqual(
            json.loads(request["body"]),
            {"entity_id": "light.hall"},
        )

    def test_brightness_rejects_invalid_values_before_any_request(self) -> None:
        for value in (-1, 101, 1.5, "bright"):
            with self.subTest(value=value):
                with self.assertRaisesRegex(ValueError, "invalid-brightness"):
                    bridge.set_brightness(self.config(), "light.kitchen", value)
                self.assertEqual(FakeHomeAssistant.requests, [])

    def test_parse_brightness_rejects_invalid_cli_values(self) -> None:
        for value in ("-1", "101", "1.5", "bright"):
            with self.subTest(value=value):
                with self.assertRaisesRegex(ValueError, "invalid-brightness"):
                    bridge.parse_brightness(value)

    def test_qml_refreshes_through_the_catalog_command(self) -> None:
        self.assertIn('command: [root.helperPath, "catalog"]', QML_SERVICE.read_text())

    def test_qml_composes_catalog_and_preferences_for_selected_entities(self) -> None:
        source = QML_SERVICE.read_text()

        self.assertIn(
            "root.catalog = Array.isArray(result.entities) ? result.entities : [];",
            source,
        )
        self.assertIn(
            "const favorites = root.fixtureMode\n"
            "            ? root.fixtureFavorites\n"
            "            : HomePreferences.favorites;",
            source,
        )
        self.assertIn("name: alias || entity.sourceName,", source)
        self.assertIn("root.selectedEntities = nextSelection;", source)

    def test_authentication_error_is_redacted(self) -> None:
        self.assertEqual(
            bridge.public_http_error(401, "sensitive response"),
            "authentication-required",
        )
        self.assertEqual(
            bridge.public_http_error(500, "sensitive response"),
            "request-failed",
        )

    def test_gsettings_array_parser_handles_empty_and_values(self) -> None:
        self.assertEqual(bridge.parse_gsettings_array("@as []"), ())
        self.assertEqual(
            bridge.parse_gsettings_array("['light.kitchen', 'light.hall']"),
            ("light.kitchen", "light.hall"),
        )


if __name__ == "__main__":
    unittest.main()
