from __future__ import annotations

import base64
import json
from pathlib import Path

from fastapi.testclient import TestClient

from python_sidecar.main import app


class FakeWatcher:
    def __init__(self) -> None:
        self.stopped = False

    def stop(self) -> None:
        self.stopped = True


class FakeTerminalSession:
    def __init__(self, session_id: str, width: int = 120, height: int = 40) -> None:
        self.session_id = session_id
        self.width = width
        self.height = height
        self.is_running = False

    def start(self) -> None:
        self.is_running = True

    def resize(self, width: int, height: int) -> None:
        self.width = width
        self.height = height

    def read_output(self, timeout: float = 0.1) -> bytes | None:
        _ = timeout
        return None

    def write_input(self, data: bytes) -> None:
        _ = data


class FakeTerminalManager:
    def __init__(self) -> None:
        self.sessions: dict[str, FakeTerminalSession] = {}

    def create_session(
        self, session_id: str, output_path: Path, width: int, height: int
    ) -> FakeTerminalSession:
        _ = output_path
        session = FakeTerminalSession(session_id, width, height)
        self.sessions[session_id] = session
        return session

    def get_session(self, session_id: str) -> FakeTerminalSession | None:
        return self.sessions.get(session_id)

    def stop_session(self, session_id: str) -> dict[str, object] | None:
        if session_id not in self.sessions:
            return None
        self.sessions.pop(session_id)
        return {
            "session_id": session_id,
            "output_path": "/tmp/fake.cast",
            "duration_ms": 100,
        }

    def list_sessions(self) -> list[str]:
        return list(self.sessions.keys())


class FakeJoinWatcher:
    def join(self) -> None:
        return None


class ErrorTerminalManager:
    def create_session(self, **_kwargs):
        raise RuntimeError("start boom")

    def stop_session(self, _session_id: str):
        raise RuntimeError("stop boom")

    def get_session(self, _session_id: str):
        return None

    def list_sessions(self) -> list[str]:
        return []


class FakeWsSession:
    def __init__(self) -> None:
        self.session_id = "ws-1"
        self.width = 120
        self.height = 40
        self._running = True
        self.read_calls = 0
        self.writes: list[bytes] = []
        self.resizes: list[tuple[int, int]] = []

    @property
    def is_running(self) -> bool:
        return self._running

    def read_output(self, timeout: float = 0.05) -> bytes | None:
        _ = timeout
        self.read_calls += 1
        if self.read_calls == 1:
            return b"hello"
        self._running = False
        return None

    def write_input(self, data: bytes) -> None:
        self.writes.append(data)

    def resize(self, width: int, height: int) -> None:
        self.width = width
        self.height = height
        self.resizes.append((width, height))


class FakeWsManager:
    def __init__(self, session: FakeWsSession | None) -> None:
        self._session = session

    def get_session(self, _session_id: str):
        return self._session

    def list_sessions(self) -> list[str]:
        return ["ws-1"] if self._session else []


client = TestClient(app)


def test_health_endpoints() -> None:
    assert client.get("/health").json() == {"status": "ok"}
    assert client.get("/api/events/health").json() == {"status": "ok", "module": "events"}
    assert client.get("/api/upload/health").json() == {"status": "ok", "module": "upload"}


def test_upload_session_stub() -> None:
    resp = client.post(
        "/api/upload/session",
        json={
            "session_id": "s1",
            "video_path": "/tmp/video.mp4",
            "events_path": "/tmp/events.ndjson",
        },
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is False
    assert "not yet implemented" in body["error"]


def test_events_start_and_stop(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import events as events_api

    watcher_a11y = FakeWatcher()
    watcher_fs = FakeWatcher()

    monkeypatch.setattr(events_api, "start_event_capture", lambda cb: watcher_a11y)
    monkeypatch.setattr(events_api, "start_fs_watcher", lambda path, cb: watcher_fs)

    session_dir = tmp_path / "session-a"
    start_resp = client.post(
        "/api/events/start",
        json={"session_dir": str(session_dir), "project_path": str(tmp_path)},
    )
    assert start_resp.status_code == 200
    payload = start_resp.json()
    assert payload["status"] == "started"
    assert Path(payload["file"]).exists()

    stop_resp = client.post("/api/events/stop")
    assert stop_resp.status_code == 200
    assert stop_resp.json()["status"] == "stopped"
    assert watcher_a11y.stopped is True
    assert watcher_fs.stopped is True


def test_events_state_and_append(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import events as events_api

    watcher = FakeWatcher()
    monkeypatch.setattr(events_api, "start_event_capture", lambda cb: watcher)

    session_dir = tmp_path / "session-dom"
    start = client.post(
        "/api/events/start",
        json={"session_dir": str(session_dir), "recording_start_ms": 1000},
    )
    assert start.status_code == 200

    state = client.get("/api/events/state")
    assert state.status_code == 200
    assert state.json()["active"] is True
    assert state.json()["event_count"] == 0

    append = client.post(
        "/api/events/append",
        json={
            "event": {
                "type": "dom_navigation",
                "timestamp_ms": 2000,
                "url": "https://example.com/search?q=hello",
                "title": "Example",
                "transition": "load",
            }
        },
    )
    assert append.status_code == 200
    assert append.json()["appended"] == 1

    events_path = session_dir / "events.ndjson"
    lines = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(lines) == 1
    parsed = json.loads(lines[0])
    assert parsed["type"] == "dom_navigation"
    assert parsed["ts_ms"] == 1000
    assert parsed["url"].startswith("https://example.com/")

    stop = client.post("/api/events/stop")
    assert stop.status_code == 200

    state2 = client.get("/api/events/state")
    assert state2.status_code == 200
    assert state2.json()["active"] is False

    append_after_stop = client.post(
        "/api/events/append",
        json={"event": {"type": "dom_navigation", "timestamp_ms": 3000}},
    )
    assert append_after_stop.status_code == 409


def test_events_stop_handles_join_only_watcher(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import events as events_api

    watcher = FakeWatcher()
    join_only = FakeJoinWatcher()
    monkeypatch.setattr(events_api, "start_event_capture", lambda cb: watcher)
    monkeypatch.setattr(events_api, "start_fs_watcher", lambda path, cb: join_only)

    session_dir = tmp_path / "session-c"
    resp = client.post(
        "/api/events/start",
        json={"session_dir": str(session_dir), "project_path": str(tmp_path)},
    )
    assert resp.status_code == 200

    stopped = client.post("/api/events/stop")
    assert stopped.status_code == 200
    assert watcher.stopped is True


def test_analysis_success_writes_file(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import analysis as analysis_api

    session_dir = tmp_path / "session-b"
    session_dir.mkdir()
    events_path = session_dir / "events.ndjson"
    events_path.write_text('{"type":"command","command":"ls"}\n', encoding="utf-8")

    class FakeAnalyzer:
        def __init__(self, api_key: str | None = None) -> None:
            self.api_key = api_key

        def analyze_recording(self, event_path: str):
            assert event_path.endswith("events.ndjson")
            return {"steps": [{"id": "1", "title": "Step", "description": "Do x"}]}

    monkeypatch.setattr(analysis_api, "RecordingAnalyzer", FakeAnalyzer)

    resp = client.post(
        "/api/analysis/analyze", json={"session_dir": str(session_dir), "api_key": "k"}
    )
    assert resp.status_code == 200
    assert resp.json()["steps"][0]["id"] == "1"

    written = json.loads((session_dir / "analysis.json").read_text(encoding="utf-8"))
    assert written["steps"][0]["title"] == "Step"


def test_analysis_404_when_events_missing(tmp_path: Path) -> None:
    resp = client.post("/api/analysis/analyze", json={"session_dir": str(tmp_path / "missing")})
    assert resp.status_code == 404


def test_analysis_500_when_analyzer_fails(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import analysis as analysis_api

    session_dir = tmp_path / "session-d"
    session_dir.mkdir()
    (session_dir / "events.ndjson").write_text("{}\n", encoding="utf-8")

    class BoomAnalyzer:
        def __init__(self, api_key: str | None = None) -> None:
            _ = api_key

        def analyze_recording(self, events_path: str):
            _ = events_path
            raise RuntimeError("analysis boom")

    monkeypatch.setattr(analysis_api, "RecordingAnalyzer", BoomAnalyzer)
    resp = client.post("/api/analysis/analyze", json={"session_dir": str(session_dir)})
    assert resp.status_code == 500


def test_localize_copy_success(monkeypatch) -> None:
    from python_sidecar.api import analysis as analysis_api

    class FakeCopyAssistant:
        def __init__(self, api_key: str | None = None) -> None:
            self.api_key = api_key

        def localize_text(
            self,
            *,
            text: str,
            target_language: str,
            source_language: str | None = None,
        ) -> str:
            assert text == "Open the settings panel"
            assert target_language == "es"
            assert source_language is None
            return "Abre el panel de configuración"

    monkeypatch.setattr(analysis_api, "CopyAssistant", FakeCopyAssistant)

    resp = client.post(
        "/api/analysis/localize-copy",
        json={"text": "Open the settings panel", "target_language": "es"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["text"] == "Abre el panel de configuración"
    assert body["target_language"] == "es"


def test_voiceover_script_success(monkeypatch) -> None:
    from python_sidecar.api import analysis as analysis_api

    class FakeCopyAssistant:
        def __init__(self, api_key: str | None = None) -> None:
            self.api_key = api_key

        def generate_voiceover_script(
            self,
            *,
            text: str,
            language: str | None = None,
            tone: str | None = None,
            max_seconds: int = 12,
        ) -> str:
            assert "Click Deploy" in text
            assert language == "en"
            assert tone == "friendly"
            assert max_seconds == 10
            return "Click Deploy to publish this build."

    monkeypatch.setattr(analysis_api, "CopyAssistant", FakeCopyAssistant)

    resp = client.post(
        "/api/analysis/voiceover-script",
        json={
            "text": "Click Deploy to publish this build.",
            "language": "en",
            "tone": "friendly",
            "max_seconds": 10,
        },
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["script"] == "Click Deploy to publish this build."
    assert body["language"] == "en"
    assert body["tone"] == "friendly"


def test_localize_copy_requires_text(monkeypatch) -> None:
    from python_sidecar.api import analysis as analysis_api

    class FakeCopyAssistant:
        def __init__(self, api_key: str | None = None) -> None:
            self.api_key = api_key

        def localize_text(self, **_kwargs) -> str:
            return "unused"

    monkeypatch.setattr(analysis_api, "CopyAssistant", FakeCopyAssistant)

    resp = client.post(
        "/api/analysis/localize-copy",
        json={"text": "   ", "target_language": "es"},
    )
    assert resp.status_code == 400


def test_export_markdown_success_and_failure(monkeypatch, tmp_path: Path) -> None:
    from python_sidecar.api import export as export_api

    class FakeGeneratorSuccess:
        def __init__(self, session_dir: str) -> None:
            self.session_dir = session_dir

        def generate(self):
            return str(Path(self.session_dir) / "tutorial.md")

    class FakeGeneratorFailure:
        def __init__(self, session_dir: str) -> None:
            self.session_dir = session_dir

        def generate(self):
            return None

    monkeypatch.setattr(export_api, "MarkdownGenerator", FakeGeneratorSuccess)
    ok = client.post("/api/export/markdown", json={"session_dir": str(tmp_path)})
    assert ok.status_code == 200
    assert ok.json()["status"] == "exported"

    monkeypatch.setattr(export_api, "MarkdownGenerator", FakeGeneratorFailure)
    bad = client.post("/api/export/markdown", json={"session_dir": str(tmp_path)})
    assert bad.status_code == 400

    class FakeGeneratorCrash:
        def __init__(self, session_dir: str) -> None:
            self.session_dir = session_dir

        def generate(self):
            raise RuntimeError("export boom")

    monkeypatch.setattr(export_api, "MarkdownGenerator", FakeGeneratorCrash)
    crashed = client.post("/api/export/markdown", json={"session_dir": str(tmp_path)})
    assert crashed.status_code == 500


def test_terminal_routes_with_fake_manager(monkeypatch) -> None:
    from python_sidecar.api import terminal as terminal_api

    manager = FakeTerminalManager()
    monkeypatch.setattr(terminal_api, "get_manager", lambda: manager)

    start = client.post(
        "/api/terminal/start",
        json={"session_id": "t1", "output_path": "/tmp/t1.cast", "width": 90, "height": 20},
    )
    assert start.status_code == 200
    assert start.json()["success"] is True

    status = client.get("/api/terminal/status/t1")
    assert status.status_code == 200
    assert status.json()["is_running"] is True

    resize = client.post("/api/terminal/resize/t1", json={"width": 100, "height": 30})
    assert resize.status_code == 200
    assert resize.json()["success"] is True

    sessions = client.get("/api/terminal/sessions")
    assert sessions.status_code == 200
    assert sessions.json()["sessions"] == ["t1"]

    health = client.get("/api/terminal/health")
    assert health.status_code == 200
    assert health.json()["active_sessions"] == 1

    stop = client.post("/api/terminal/stop/t1")
    assert stop.status_code == 200
    assert stop.json()["success"] is True

    missing = client.get("/api/terminal/status/missing")
    assert missing.status_code == 200
    assert missing.json()["error"] == "Session not found"

    missing_resize = client.post("/api/terminal/resize/missing", json={"width": 10, "height": 5})
    assert missing_resize.status_code == 200
    assert missing_resize.json()["success"] is False


def test_terminal_start_and_stop_error_paths(monkeypatch) -> None:
    from python_sidecar.api import terminal as terminal_api

    manager = ErrorTerminalManager()
    monkeypatch.setattr(terminal_api, "get_manager", lambda: manager)

    start = client.post(
        "/api/terminal/start", json={"session_id": "err", "output_path": "/tmp/out.cast"}
    )
    assert start.status_code == 200
    assert start.json()["success"] is False
    assert "start boom" in start.json()["error"]

    stop = client.post("/api/terminal/stop/err")
    assert stop.status_code == 200
    assert stop.json()["success"] is False
    assert "stop boom" in stop.json()["error"]


def test_terminal_websocket_stream_and_control(monkeypatch) -> None:
    from python_sidecar.api import terminal as terminal_api

    ws_session = FakeWsSession()
    manager = FakeWsManager(ws_session)
    monkeypatch.setattr(terminal_api, "get_manager", lambda: manager)

    with client.websocket_connect("/api/terminal/ws/ws-1") as websocket:
        websocket.send_json({"type": "input", "data": base64.b64encode(b"ls\n").decode("ascii")})
        websocket.send_json({"type": "resize", "cols": 140, "rows": 50})

        output = websocket.receive_json()
        assert output["type"] == "output"
        assert base64.b64decode(output["data"]) == b"hello"

        exit_msg = websocket.receive_json()
        assert exit_msg["type"] == "exit"

    assert ws_session.writes == [b"ls\n"]
    assert ws_session.resizes[-1] == (140, 50)


def test_terminal_websocket_missing_session(monkeypatch) -> None:
    from python_sidecar.api import terminal as terminal_api

    monkeypatch.setattr(terminal_api, "get_manager", lambda: FakeWsManager(None))

    with client.websocket_connect("/api/terminal/ws/missing") as websocket:
        msg = websocket.receive_json()
        assert msg["type"] == "error"
        assert msg["message"] == "Session not found"
