from __future__ import annotations

import shlex
import sqlite3
import subprocess
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from factory_menu import build_factory_menu_items, factory_status_label
from factory_status import (
    DEFAULT_DB_PATH,
    FactoryActivity,
    FactoryAvailability,
    FactoryLiveRun,
    FactoryPendingDecision,
    FactoryStatusSnapshot,
    argv_matches_recorded_subsequence,
    deck_factory_url,
    load_factory_status,
    read_proc_argv,
)
from tray_model import TrayMenuItemKind

REPO_A = "/home/user/Projects/overdeck"
REPO_B = "/home/user/Projects/multideal"

SCHEMA = """
CREATE TABLE sessions (
  adw_id TEXT PRIMARY KEY,
  adw_name TEXT,
  repo TEXT,
  request TEXT,
  status TEXT,
  engineer TEXT,
  started_at TEXT,
  ended_at TEXT,
  total_tokens INTEGER DEFAULT 0,
  total_cost REAL DEFAULT 0,
  archived INTEGER DEFAULT 0
);
CREATE TABLE processes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  adw_id TEXT,
  kind TEXT,
  name TEXT,
  pid INTEGER,
  command TEXT,
  started_at TEXT,
  ended_at TEXT
);
"""

LEGACY_SCHEMA = """
CREATE TABLE sessions (
  adw_id TEXT PRIMARY KEY,
  adw_name TEXT,
  request TEXT,
  status TEXT,
  engineer TEXT,
  started_at TEXT,
  ended_at TEXT
);
CREATE TABLE processes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  adw_id TEXT,
  kind TEXT,
  name TEXT,
  pid INTEGER,
  command TEXT,
  started_at TEXT,
  ended_at TEXT
);
"""


def _create_db(path: Path, schema: str = SCHEMA) -> sqlite3.Connection:
    conn = sqlite3.connect(path)
    conn.executescript(schema)
    conn.execute("PRAGMA journal_mode = WAL")
    conn.execute("PRAGMA synchronous = NORMAL")
    conn.execute("PRAGMA busy_timeout = 5000")
    return conn


def _insert_running_session(
    conn: sqlite3.Connection,
    *,
    adw_id: str,
    repo: str,
    with_repo_column: bool = True,
) -> None:
    if with_repo_column:
        conn.execute(
            "INSERT INTO sessions (adw_id, adw_name, repo, request, status, started_at)"
            " VALUES (?, ?, ?, ?, 'running', '2026-08-05T10:00:00Z')",
            (adw_id, "adw_test", repo, f"request for {adw_id}"),
        )
    else:
        conn.execute(
            "INSERT INTO sessions (adw_id, adw_name, request, status, started_at)"
            " VALUES (?, ?, ?, 'running', '2026-08-05T10:00:00Z')",
            (adw_id, "adw_test", f"request for {adw_id}"),
        )
    conn.commit()


def _insert_process(
    conn: sqlite3.Connection,
    *,
    adw_id: str,
    pid: int,
    command: str,
) -> None:
    conn.execute(
        "INSERT INTO processes (adw_id, kind, name, pid, command, started_at)"
        " VALUES (?, 'agent', 'coder', ?, ?, '2026-08-05T10:00:00Z')",
        (adw_id, pid, command),
    )
    conn.commit()


def test_missing_db_returns_unavailable_idle_safe(tmp_path: Path) -> None:
    missing = tmp_path / "missing.db"
    snapshot = load_factory_status(missing, exists_impl=lambda _path: False)

    assert snapshot.availability == FactoryAvailability.UNAVAILABLE
    assert snapshot.activity == FactoryActivity.IDLE
    assert snapshot.live_runs == ()
    assert snapshot.db_present is False


def test_idle_when_no_running_sessions(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    conn.execute(
        "INSERT INTO sessions (adw_id, adw_name, repo, request, status, started_at, ended_at)"
        " VALUES ('done-1', 'adw_test', ?, 'done', 'success', '2026-08-05T09:00:00Z', '2026-08-05T10:00:00Z')",
        (REPO_A,),
    )
    conn.commit()
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.availability == FactoryAvailability.AVAILABLE
    assert snapshot.activity == FactoryActivity.IDLE
    assert snapshot.live_runs == ()
    assert snapshot.db_present is True


def test_live_multi_project_reports_canonical_repos(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    _insert_running_session(conn, adw_id="run-a", repo=REPO_A)
    _insert_running_session(conn, adw_id="run-b", repo=REPO_B)

    proc_a = subprocess.Popen(
        [sys.executable, "-c", "import time; time.sleep(120)"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    proc_b = subprocess.Popen(
        [sys.executable, "-c", "import time; time.sleep(120)"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        live_a = read_proc_argv(proc_a.pid)
        live_b = read_proc_argv(proc_b.pid)
        assert live_a is not None
        assert live_b is not None
        _insert_process(
            conn,
            adw_id="run-a",
            pid=proc_a.pid,
            command=shlex.join(live_a),
        )
        _insert_process(
            conn,
            adw_id="run-b",
            pid=proc_b.pid,
            command=shlex.join(live_b),
        )

        snapshot = load_factory_status(db_path)

        assert snapshot.activity == FactoryActivity.LIVE
        assert {run.repo for run in snapshot.live_runs} == {REPO_A, REPO_B}
    finally:
        for proc in (proc_a, proc_b):
            if proc.poll() is None:
                proc.terminate()
            proc.wait(timeout=5)


def test_stale_absent_pid_is_not_live(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    _insert_running_session(conn, adw_id="stale-absent", repo=REPO_A)
    _insert_process(conn, adw_id="stale-absent", pid=424242, command="factory child")

    snapshot = load_factory_status(db_path)

    assert snapshot.activity == FactoryActivity.IDLE
    assert snapshot.live_runs == ()


def test_mismatched_reused_pid_is_not_live(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    _insert_running_session(conn, adw_id="stale-mismatch", repo=REPO_A)

    proc = subprocess.Popen(
        [sys.executable, "-c", "import time; time.sleep(120)"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        _insert_process(conn, adw_id="stale-mismatch", pid=proc.pid, command="stop_child.py child-a")

        snapshot = load_factory_status(db_path)

        assert snapshot.activity == FactoryActivity.IDLE
        assert snapshot.live_runs == ()
    finally:
        if proc.poll() is None:
            proc.terminate()
        proc.wait(timeout=5)


def test_legacy_schema_without_repo_column_still_loads_idle(tmp_path: Path) -> None:
    db_path = tmp_path / "legacy.db"
    conn = _create_db(db_path, LEGACY_SCHEMA)
    _insert_running_session(conn, adw_id="legacy", repo=REPO_A, with_repo_column=False)
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.availability == FactoryAvailability.AVAILABLE
    assert snapshot.activity == FactoryActivity.IDLE


def test_malformed_schema_returns_unavailable(tmp_path: Path) -> None:
    db_path = tmp_path / "malformed.db"
    conn = sqlite3.connect(db_path)
    conn.execute("CREATE TABLE sessions (adw_id TEXT PRIMARY KEY)")
    conn.commit()
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.availability == FactoryAvailability.UNAVAILABLE
    assert snapshot.activity == FactoryActivity.IDLE


def test_busy_db_returns_unavailable_idle_safe(tmp_path: Path) -> None:
    db_path = tmp_path / "busy.db"
    conn = _create_db(db_path)
    conn.close()

    def failing_open(_path: Path) -> sqlite3.Connection:
        raise sqlite3.OperationalError("database is locked")

    snapshot = load_factory_status(db_path, open_db_impl=failing_open)

    assert snapshot.availability == FactoryAvailability.UNAVAILABLE
    assert snapshot.activity == FactoryActivity.IDLE


def test_menu_labels_for_idle_live_and_unavailable() -> None:
    idle = FactoryStatusSnapshot(
        availability=FactoryAvailability.AVAILABLE,
        activity=FactoryActivity.IDLE,
        live_runs=(),
        db_path=str(DEFAULT_DB_PATH),
        db_present=True,
    )
    live = FactoryStatusSnapshot(
        availability=FactoryAvailability.AVAILABLE,
        activity=FactoryActivity.LIVE,
        live_runs=(
            FactoryLiveRun(adw_id="a", repo=REPO_A),
            FactoryLiveRun(adw_id="b", repo=REPO_B),
        ),
        db_path=str(DEFAULT_DB_PATH),
        db_present=True,
    )
    unavailable = FactoryStatusSnapshot.unavailable()

    assert factory_status_label(idle) == "Status: idle"
    assert factory_status_label(live) == "Status: live (2 runs)"
    assert factory_status_label(unavailable) == "Status: unavailable"

    idle_menu = build_factory_menu_items(idle)[0]
    live_menu = build_factory_menu_items(live)[0]
    unavailable_menu = build_factory_menu_items(unavailable)[0]

    assert idle_menu.kind == TrayMenuItemKind.SUBMENU
    assert idle_menu.label == "Factory"
    assert idle_menu.children[0].label == "Status: idle"
    assert idle_menu.children[-1].action == "open_factory"
    assert idle_menu.children[-1].sensitive is True

    assert [child.label for child in live_menu.children if child.label.startswith("/")] == [
        REPO_A,
        REPO_B,
    ]
    assert unavailable_menu.children[-1].sensitive is True


DECISIONS_SCHEMA = """
CREATE TABLE decisions (
  decision_id  TEXT PRIMARY KEY,
  adw_id       TEXT NOT NULL,
  phase        TEXT,
  question     TEXT NOT NULL,
  options      TEXT NOT NULL DEFAULT '[]',
  free_text    INTEGER NOT NULL DEFAULT 0,
  context      TEXT NOT NULL DEFAULT '',
  status       TEXT NOT NULL DEFAULT 'pending',
  answer_value TEXT,
  answer_text  TEXT,
  answered_by  TEXT,
  created_at   TEXT NOT NULL,
  answered_at  TEXT
);
"""


def test_pending_decisions_loaded_in_created_order(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    conn.executescript(DECISIONS_SCHEMA)
    _insert_running_session(conn, adw_id="run-dec", repo=REPO_A)
    conn.execute(
        "INSERT INTO decisions (decision_id, adw_id, question, status, created_at)"
        " VALUES ('dec-late', 'run-dec', 'Second?', 'pending', '2026-08-05T10:10:00Z')",
    )
    conn.execute(
        "INSERT INTO decisions (decision_id, adw_id, question, status, created_at)"
        " VALUES ('dec-early', 'run-dec', 'First?', 'pending', '2026-08-05T10:05:00Z')",
    )
    conn.execute(
        "INSERT INTO decisions (decision_id, adw_id, question, status, created_at, answered_at)"
        " VALUES ('dec-done', 'run-dec', 'Old?', 'answered', '2026-08-05T10:00:00Z', '2026-08-05T10:01:00Z')",
    )
    conn.commit()
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.pending_decisions == (
        FactoryPendingDecision(
            decision_id="dec-early",
            adw_id="run-dec",
            question="First?",
            created_at="2026-08-05T10:05:00Z",
        ),
        FactoryPendingDecision(
            decision_id="dec-late",
            adw_id="run-dec",
            question="Second?",
            created_at="2026-08-05T10:10:00Z",
        ),
    )


def test_pending_decisions_empty_without_decisions_table(tmp_path: Path) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    _insert_running_session(conn, adw_id="run-plain", repo=REPO_A)
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.availability == FactoryAvailability.AVAILABLE
    assert snapshot.pending_decisions == ()
    assert snapshot.all_pending_decision_ids == ()


def test_archived_session_decision_excluded_from_pending_included_in_all_ids(
    tmp_path: Path,
) -> None:
    db_path = tmp_path / "sssf.db"
    conn = _create_db(db_path)
    conn.executescript(DECISIONS_SCHEMA)
    _insert_running_session(conn, adw_id="run-archived", repo=REPO_A)
    conn.execute("UPDATE sessions SET archived = 1 WHERE adw_id = 'run-archived'")
    conn.execute(
        "INSERT INTO decisions (decision_id, adw_id, question, status, created_at)"
        " VALUES ('dec-archived', 'run-archived', 'Archived?', 'pending', '2026-08-05T10:00:00Z')",
    )
    conn.commit()
    conn.close()

    snapshot = load_factory_status(db_path)

    assert snapshot.pending_decisions == ()
    assert snapshot.all_pending_decision_ids == ("dec-archived",)


def test_menu_shows_pending_decision_row() -> None:
    snapshot = FactoryStatusSnapshot(
        availability=FactoryAvailability.AVAILABLE,
        activity=FactoryActivity.IDLE,
        live_runs=(),
        db_path=str(DEFAULT_DB_PATH),
        db_present=True,
        pending_decisions=(
            FactoryPendingDecision(
                decision_id="dec-1",
                adw_id="run-a",
                question="Which backend?",
                created_at="2026-08-05T10:05:00Z",
            ),
        ),
    )

    menu = build_factory_menu_items(snapshot)[0]
    labels = [child.label for child in menu.children if child.label]
    assert "1 decision waiting — open deck to answer" in labels
    row = next(child for child in menu.children if child.label == "1 decision waiting — open deck to answer")
    assert row.action == "open_factory"
    assert row.sensitive is True


def test_deck_factory_url_uses_overdeck_web_url_env(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("OVERDECK_WEB_URL", "http://127.0.0.1:31337")
    assert deck_factory_url() == "http://127.0.0.1:31337/factory"


def test_identity_helper_rejects_unrelated_module_launch() -> None:
    proc_argv = [sys.executable, "-m", "pytest", "tests", "-q"]
    recorded = f"{sys.executable} {Path(pytest.__file__).resolve().parent / '__main__.py'} tests -q"
    assert not argv_matches_recorded_subsequence(proc_argv, recorded)
