from __future__ import annotations

import json
import uuid
from pathlib import Path
from types import SimpleNamespace

import pytest

from codex_autorunner.core.flows import FlowStore
from codex_autorunner.core.flows.models import FlowRunStatus
from codex_autorunner.integrations.telegram.adapter import TelegramMessage
from codex_autorunner.integrations.telegram.handlers.commands import (
    flows as flows_module,
)
from codex_autorunner.integrations.telegram.handlers.commands.flows import FlowCommands


class _NowSeq:
    def __init__(self) -> None:
        self._counter = 0

    def __call__(self) -> str:
        self._counter += 1
        return f"2026-01-30T00:00:0{self._counter}Z"


class _FlowServiceStub:
    def __init__(self) -> None:
        self.start_calls: list[dict[str, object]] = []
        self.resume_calls: list[str] = []
        self.stop_calls: list[str] = []
        self.ensure_calls: list[tuple[str, bool]] = []
        self.reconcile_calls: list[str] = []

    async def start_flow_run(
        self,
        _flow_target_id: str,
        *,
        input_data: dict[str, object] | None = None,
        metadata: dict[str, object] | None = None,
        run_id: str | None = None,
    ) -> SimpleNamespace:
        self.start_calls.append(
            {
                "input_data": input_data or {},
                "metadata": metadata or {},
                "run_id": run_id,
            }
        )
        return SimpleNamespace(run_id=run_id or "run-1")

    async def resume_flow_run(
        self, run_id: str, *, force: bool = False
    ) -> SimpleNamespace:
        self.resume_calls.append(run_id)
        return SimpleNamespace(run_id=run_id, status="running", force=force)

    async def stop_flow_run(self, run_id: str) -> SimpleNamespace:
        self.stop_calls.append(run_id)
        return SimpleNamespace(run_id=run_id, status="stopped")

    def ensure_flow_run_worker(self, run_id: str, *, is_terminal: bool = False) -> None:
        self.ensure_calls.append((run_id, is_terminal))

    def reconcile_flow_run(self, run_id: str) -> tuple[SimpleNamespace, bool, bool]:
        self.reconcile_calls.append(run_id)
        return SimpleNamespace(run_id=run_id, status="running"), True, False


class _FlowLifecycleHandler(FlowCommands):
    def __init__(self) -> None:
        self.sent: list[str] = []
        self.reply_markups: list[dict[str, object] | None] = []

    async def _send_message(
        self,
        _chat_id: int,
        text: str,
        *,
        thread_id: int | None = None,
        reply_to: int | None = None,
        reply_markup: dict[str, object] | None = None,
        parse_mode: str | None = None,
    ) -> None:
        _ = (thread_id, reply_to, parse_mode)
        self.sent.append(text)
        self.reply_markups.append(reply_markup)


def _message() -> TelegramMessage:
    return TelegramMessage(
        update_id=1,
        message_id=10,
        chat_id=999,
        thread_id=123,
        from_user_id=1,
        text="/flow",
        date=None,
        is_topic_message=True,
    )


def _init_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> FlowStore:
    from codex_autorunner.core.flows import store as store_module

    monkeypatch.setattr(store_module, "now_iso", _NowSeq())
    db_path = tmp_path / ".codex-autorunner" / "flows.db"
    db_path.parent.mkdir(parents=True, exist_ok=True)
    store = FlowStore(db_path)
    store.initialize()
    return store


def _create_run(store: FlowStore, run_id: str, status: FlowRunStatus) -> None:
    store.create_flow_run(run_id, "ticket_flow", {})
    store.update_flow_run_status(run_id, status)


@pytest.mark.anyio
async def test_flow_resume_defaults_latest_paused(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_old = str(uuid.uuid4())
    run_new = str(uuid.uuid4())
    _create_run(store, run_old, FlowRunStatus.PAUSED)
    _create_run(store, run_new, FlowRunStatus.PAUSED)
    store.close()

    flow_service = _FlowServiceStub()
    monkeypatch.setattr(
        flows_module,
        "build_ticket_flow_orchestration_service",
        lambda *, workspace_root: flow_service,
    )

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_resume(_message(), tmp_path, argv=[])

    assert flow_service.resume_calls == [run_new]
    assert any(f"Resumed run `{run_new}`" in text for text in handler.sent)


@pytest.mark.anyio
async def test_flow_stop_defaults_latest_active(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_running = str(uuid.uuid4())
    run_completed = str(uuid.uuid4())
    _create_run(store, run_running, FlowRunStatus.RUNNING)
    _create_run(store, run_completed, FlowRunStatus.COMPLETED)
    store.close()

    flow_service = _FlowServiceStub()
    monkeypatch.setattr(
        flows_module,
        "build_ticket_flow_orchestration_service",
        lambda *, workspace_root: flow_service,
    )

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_stop(_message(), tmp_path, argv=[])

    assert flow_service.stop_calls == [run_running]
    assert any(f"Stopped run `{run_running}`" in text for text in handler.sent)
    inbound_path = (
        tmp_path
        / ".codex-autorunner"
        / "flows"
        / run_running
        / "chat"
        / "inbound.jsonl"
    )
    outbound_path = (
        tmp_path
        / ".codex-autorunner"
        / "flows"
        / run_running
        / "chat"
        / "outbound.jsonl"
    )
    inbound_records = [
        json.loads(line)
        for line in inbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    outbound_records = [
        json.loads(line)
        for line in outbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    assert inbound_records[-1]["event_type"] == "flow_stop_command"
    assert inbound_records[-1]["kind"] == "command"
    assert outbound_records[-1]["event_type"] == "flow_stop_notice"
    assert outbound_records[-1]["kind"] == "notice"


@pytest.mark.anyio
async def test_flow_resume_mirrors_chat_inbound_and_outbound(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_id = str(uuid.uuid4())
    _create_run(store, run_id, FlowRunStatus.PAUSED)
    store.close()

    flow_service = _FlowServiceStub()
    monkeypatch.setattr(
        flows_module,
        "build_ticket_flow_orchestration_service",
        lambda *, workspace_root: flow_service,
    )

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_resume(_message(), tmp_path, argv=[])

    assert flow_service.resume_calls == [run_id]

    inbound_path = (
        tmp_path / ".codex-autorunner" / "flows" / run_id / "chat" / "inbound.jsonl"
    )
    outbound_path = (
        tmp_path / ".codex-autorunner" / "flows" / run_id / "chat" / "outbound.jsonl"
    )
    assert inbound_path.exists()
    assert outbound_path.exists()

    inbound_records = [
        json.loads(line)
        for line in inbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    outbound_records = [
        json.loads(line)
        for line in outbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    assert len(inbound_records) == 1
    assert len(outbound_records) == 1
    assert inbound_records[0]["event_type"] == "flow_resume_command"
    assert inbound_records[0]["kind"] == "command"
    assert inbound_records[0]["actor"] == "user"
    assert outbound_records[0]["event_type"] == "flow_resume_notice"
    assert outbound_records[0]["kind"] == "notice"
    assert outbound_records[0]["actor"] == "car"
    assert inbound_records[0]["meta"]["run_id"] == run_id
    assert outbound_records[0]["meta"]["run_id"] == run_id

    with FlowStore(tmp_path / ".codex-autorunner" / "flows.db") as verify_store:
        artifact_kinds = {
            artifact.kind for artifact in verify_store.get_artifacts(run_id)
        }
    assert "chat_inbound" in artifact_kinds
    assert "chat_outbound" in artifact_kinds


@pytest.mark.anyio
async def test_flow_recover_defaults_latest_active(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_running = str(uuid.uuid4())
    run_completed = str(uuid.uuid4())
    _create_run(store, run_running, FlowRunStatus.RUNNING)
    _create_run(store, run_completed, FlowRunStatus.COMPLETED)
    store.close()

    flow_service = _FlowServiceStub()
    monkeypatch.setattr(
        flows_module,
        "build_ticket_flow_orchestration_service",
        lambda *, workspace_root: flow_service,
    )

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_recover(_message(), tmp_path, argv=[])

    assert flow_service.reconcile_calls == [run_running]
    assert any("Recovered" in text for text in handler.sent)


@pytest.mark.anyio
async def test_flow_archive_defaults_latest_paused_prompts_confirmation(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_terminal = str(uuid.uuid4())
    run_paused = str(uuid.uuid4())
    _create_run(store, run_terminal, FlowRunStatus.COMPLETED)
    _create_run(store, run_paused, FlowRunStatus.PAUSED)
    store.close()

    tickets_dir = tmp_path / ".codex-autorunner" / "tickets"
    tickets_dir.mkdir(parents=True, exist_ok=True)
    (tickets_dir / "TICKET-001.md").write_text("ticket", encoding="utf-8")

    context_dir = tmp_path / ".codex-autorunner" / "contextspace"
    context_dir.mkdir(parents=True, exist_ok=True)
    (context_dir / "active_context.md").write_text("Active context\n", encoding="utf-8")
    (context_dir / "decisions.md").write_text("Decision log\n", encoding="utf-8")

    run_dir = tmp_path / ".codex-autorunner" / "runs" / run_paused
    run_dir.mkdir(parents=True, exist_ok=True)
    (run_dir / "DISPATCH.md").write_text("dispatch", encoding="utf-8")
    live_flow_dir = tmp_path / ".codex-autorunner" / "flows" / run_paused / "chat"
    live_flow_dir.mkdir(parents=True, exist_ok=True)
    (live_flow_dir / "outbound.jsonl").write_text("{}", encoding="utf-8")

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_archive(_message(), tmp_path, argv=[])

    assert any("Archive it anyway?" in text for text in handler.sent)
    assert handler.reply_markups[-1] is not None
    archive_dir = (
        tmp_path
        / ".codex-autorunner"
        / "archive"
        / "runs"
        / run_paused
        / "archived_tickets"
    )
    assert not archive_dir.exists()
    assert (tickets_dir / "TICKET-001.md").exists()
    assert (tmp_path / ".codex-autorunner" / "flows" / run_paused).exists()
    store = FlowStore(tmp_path / ".codex-autorunner" / "flows.db")
    store.initialize()
    try:
        assert store.get_flow_run(run_paused) is not None
    finally:
        store.close()


@pytest.mark.anyio
async def test_flow_archive_force_archives_latest_paused(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    store = _init_store(tmp_path, monkeypatch)
    run_terminal = str(uuid.uuid4())
    run_paused = str(uuid.uuid4())
    _create_run(store, run_terminal, FlowRunStatus.COMPLETED)
    _create_run(store, run_paused, FlowRunStatus.PAUSED)
    store.close()

    tickets_dir = tmp_path / ".codex-autorunner" / "tickets"
    tickets_dir.mkdir(parents=True, exist_ok=True)
    (tickets_dir / "TICKET-001.md").write_text("ticket", encoding="utf-8")

    context_dir = tmp_path / ".codex-autorunner" / "contextspace"
    context_dir.mkdir(parents=True, exist_ok=True)
    (context_dir / "active_context.md").write_text("Active context\n", encoding="utf-8")
    (context_dir / "decisions.md").write_text("Decision log\n", encoding="utf-8")

    run_dir = tmp_path / ".codex-autorunner" / "runs" / run_paused
    run_dir.mkdir(parents=True, exist_ok=True)
    (run_dir / "DISPATCH.md").write_text("dispatch", encoding="utf-8")
    live_flow_dir = tmp_path / ".codex-autorunner" / "flows" / run_paused / "chat"
    live_flow_dir.mkdir(parents=True, exist_ok=True)
    (live_flow_dir / "outbound.jsonl").write_text("{}", encoding="utf-8")

    handler = _FlowLifecycleHandler()
    await handler._handle_flow_archive(_message(), tmp_path, argv=["--force"])

    archive_dir = (
        tmp_path
        / ".codex-autorunner"
        / "archive"
        / "runs"
        / run_paused
        / "archived_tickets"
    )
    assert (archive_dir / "TICKET-001.md").exists()
    archived_runs = (
        tmp_path
        / ".codex-autorunner"
        / "archive"
        / "runs"
        / run_paused
        / "archived_runs"
    )
    assert archived_runs.exists()
    assert (
        tmp_path
        / ".codex-autorunner"
        / "archive"
        / "runs"
        / run_paused
        / "contextspace"
        / "active_context.md"
    ).read_text(encoding="utf-8") == "Active context\n"
    assert (
        tmp_path
        / ".codex-autorunner"
        / "archive"
        / "runs"
        / run_paused
        / "flow_state"
        / "chat"
        / "outbound.jsonl"
    ).read_text(encoding="utf-8") == "{}"
    assert (
        tmp_path / ".codex-autorunner" / "contextspace" / "active_context.md"
    ).read_text(encoding="utf-8") == ""
    assert (tmp_path / ".codex-autorunner" / "contextspace" / "decisions.md").read_text(
        encoding="utf-8"
    ) == ""
    assert not (tmp_path / ".codex-autorunner" / "flows" / run_paused).exists()
    store = FlowStore(tmp_path / ".codex-autorunner" / "flows.db")
    store.initialize()
    try:
        assert store.get_flow_run(run_paused) is None
    finally:
        store.close()


@pytest.mark.anyio
async def test_flow_reply_mirrors_chat_inbound_and_outbound(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    run_id = str(uuid.uuid4())
    paused_record = SimpleNamespace(
        id=run_id, status=FlowRunStatus.PAUSED, input_data={}
    )
    flow_service = _FlowServiceStub()

    async def _get_topic(_key: str):
        return SimpleNamespace(workspace_path=str(tmp_path))

    async def _write_user_reply(
        _repo_root: Path,
        _run_id: str,
        _run_record: object,
        _message: TelegramMessage,
        _text: str,
    ) -> tuple[bool, str]:
        return True, f"Reply saved for {run_id}"

    async def _resolve_topic_key(*_args, **_kwargs) -> str:
        return "topic-key"

    handler = _FlowLifecycleHandler()
    handler._store = SimpleNamespace(get_topic=_get_topic)
    handler._ticket_flow_pause_targets = {}
    handler._get_paused_ticket_flow = lambda *_args, **_kwargs: (run_id, paused_record)  # type: ignore[assignment]
    handler._write_user_reply_from_telegram = _write_user_reply  # type: ignore[assignment]
    handler._resolve_topic_key = _resolve_topic_key  # type: ignore[assignment]
    monkeypatch.setattr(
        flows_module,
        "build_ticket_flow_orchestration_service",
        lambda *, workspace_root: flow_service,
    )

    message = TelegramMessage(
        update_id=2,
        message_id=11,
        chat_id=999,
        thread_id=123,
        from_user_id=1,
        text="/flow reply hello",
        date=None,
        is_topic_message=True,
    )
    await handler._handle_reply(message, "hello")
    assert flow_service.resume_calls == [run_id]
    assert any("Resumed run" in text for text in handler.sent)

    inbound_path = (
        tmp_path / ".codex-autorunner" / "flows" / run_id / "chat" / "inbound.jsonl"
    )
    outbound_path = (
        tmp_path / ".codex-autorunner" / "flows" / run_id / "chat" / "outbound.jsonl"
    )
    assert inbound_path.exists()
    assert outbound_path.exists()
    inbound_records = [
        json.loads(line)
        for line in inbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    outbound_records = [
        json.loads(line)
        for line in outbound_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    assert inbound_records[-1]["event_type"] == "flow_reply_command"
    assert inbound_records[-1]["kind"] == "command"
    assert inbound_records[-1]["actor"] == "user"
    assert outbound_records[-1]["event_type"] == "flow_reply_notice"
    assert outbound_records[-1]["kind"] == "notice"
    assert outbound_records[-1]["actor"] == "car"


@pytest.mark.anyio
async def test_flow_restart_rejects_invalid_run_id(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    _ = _init_store(tmp_path, monkeypatch).close()
    handler = _FlowLifecycleHandler()
    bootstrap_calls: list[list[str]] = []

    async def _fake_bootstrap(
        _message: TelegramMessage, _repo_root: Path, argv: list[str]
    ) -> None:
        bootstrap_calls.append(argv)

    monkeypatch.setattr(handler, "_handle_flow_bootstrap", _fake_bootstrap)
    await handler._handle_flow_restart(_message(), tmp_path, argv=["not-a-run-id"])

    assert any("Invalid run_id." in text for text in handler.sent)
    assert bootstrap_calls == []
