"""Tests for the global factory CLI entrypoint."""

from __future__ import annotations

import json
import os
import shlex
import shutil
import signal
import sqlite3
import subprocess
import time
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
FACTORY_BIN = ROOT / "bin" / "factory"
PROBE_ADW = Path(__file__).resolve().parent / "fixtures" / "adw_session_probe.py"
CONFIG_TEMPLATE = Path(__file__).resolve().parent / "fixtures" / "test-sssf.config.yaml"


@pytest.fixture
def factory_env(tmp_path: Path) -> dict[str, str]:
    factory_root = tmp_path / "factory-root"
    factory_root.mkdir()
    for name in ROOT.glob("adw_*.py"):
        (factory_root / name.name).symlink_to(name)
    (factory_root / "adw_modules").symlink_to(ROOT / "adw_modules")
    (factory_root / "prompt_engineering").symlink_to(ROOT / "prompt_engineering")
    (factory_root / "pyproject.toml").symlink_to(ROOT / "pyproject.toml")
    if (ROOT / "uv.lock").exists():
        (factory_root / "uv.lock").symlink_to(ROOT / "uv.lock")
    probe_target = factory_root / "adw_session_probe.py"
    probe_target.write_text(PROBE_ADW.read_text())
    probe_target.chmod(0o755)
    (factory_root / "bin").mkdir()
    (factory_root / "bin" / "factory").symlink_to(FACTORY_BIN)

    data_dir = tmp_path / "factory-data"
    db_path = tmp_path / "sssf.db"
    config_text = CONFIG_TEMPLATE.read_text()
    config_text = config_text.replace("PLACEHOLDER_DATA_DIR", str(data_dir))
    config_text = config_text.replace("PLACEHOLDER_DB", str(db_path))
    default_config = factory_root / "sssf.config.yaml"
    default_config.write_text(config_text)

    return {
        "FACTORY_ROOT": str(factory_root),
        "FACTORY_TEST_DB": str(db_path),
        "HOME": str(tmp_path / "home"),
        "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
    }


def _init_git_repo(path: Path) -> None:
    subprocess.run(["git", "init", "-q", "-b", "main", str(path)], check=True)
    subprocess.run(
        ["git", "-C", str(path), "config", "user.email", "factory@test"],
        check=True,
    )
    subprocess.run(
        ["git", "-C", str(path), "config", "user.name", "factory"],
        check=True,
    )


def _run_factory(
    *args: str,
    cwd: Path,
    env: dict[str, str],
) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [str(FACTORY_BIN), *args],
        cwd=cwd,
        env={**os.environ, **env},
        text=True,
        capture_output=True,
    )


def _install_uv_argv_probe(tmp_path: Path, env: dict[str, str]) -> Path:
    real_uv = shutil.which("uv")
    assert real_uv is not None, "uv must be on PATH for factory CLI tests"
    probe_dir = tmp_path / "uv-probe-bin"
    probe_dir.mkdir()
    probe_log = tmp_path / "uv-argv.jsonl"
    shim = probe_dir / "uv"
    probe_log_str = str(probe_log)
    shim.write_text(
        f"""#!/usr/bin/env bash
set -euo pipefail
python3 -c "import json,sys; open({probe_log_str!r}, 'a').write(json.dumps(sys.argv[1:]) + chr(10))" uv "$@"
exec {shlex.quote(real_uv)} "$@"
"""
    )
    shim.chmod(0o755)
    env["PATH"] = f"{probe_dir}{os.pathsep}{env['PATH']}"
    return probe_log


def _latest_uv_argv(probe_log: Path) -> list[str]:
    lines = [line for line in probe_log.read_text().splitlines() if line.strip()]
    assert lines, "expected factory to invoke uv exactly once"
    return json.loads(lines[-1])


def _session_repo(db_path: Path, adw_id: str = "probe-session") -> str | None:
    conn = sqlite3.connect(db_path)
    try:
        row = conn.execute(
            "SELECT repo FROM sessions WHERE adw_id=?",
            (adw_id,),
        ).fetchone()
        return row[0] if row else None
    finally:
        conn.close()


def _request_run_links(db_path: Path) -> list[tuple[str, str]]:
    conn = sqlite3.connect(db_path)
    try:
        return conn.execute(
            "SELECT request_id, adw_id FROM request_run_links ORDER BY request_id, adw_id"
        ).fetchall()
    finally:
        conn.close()


def test_temp_repo_without_project_config_uses_defaults_and_records_repo(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)

    db_path = Path(factory_env["FACTORY_TEST_DB"])
    result = _run_factory(
        "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    assert _session_repo(db_path) == str(repo.resolve())


def test_explicit_request_id_is_linked_and_inherited_identity_is_cleared(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)
    db_path = Path(factory_env["FACTORY_TEST_DB"])

    linked = _run_factory(
        "--request-id", "manual-request-42", "adw_session_probe",
        cwd=repo,
        env={**factory_env, "FACTORY_REQUEST_ID": "inherited-wrong-request"},
    )
    assert linked.returncode == 0, linked.stderr
    assert _request_run_links(db_path) == [("manual-request-42", "probe-session")]

    for suffix in ("", "-wal", "-shm"):
        Path(f"{db_path}{suffix}").unlink(missing_ok=True)
    unlinked = _run_factory(
        "adw_session_probe",
        cwd=repo,
        env={**factory_env, "FACTORY_REQUEST_ID": "inherited-wrong-request"},
    )
    assert unlinked.returncode == 0, unlinked.stderr
    assert _request_run_links(db_path) == []


def test_rejects_invalid_request_id_before_launch(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)

    result = _run_factory(
        "--request-id", "request with spaces", "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )
    assert result.returncode == 2
    assert "--request-id must be 1-240 characters" in result.stderr
    assert not Path(factory_env["FACTORY_TEST_DB"]).exists()


def test_nested_cwd_resolves_repo_root(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    nested = repo / "packages" / "app"
    nested.mkdir(parents=True)
    _init_git_repo(repo)

    db_path = Path(factory_env["FACTORY_TEST_DB"])
    result = _run_factory(
        "adw_session_probe",
        cwd=nested,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    assert _session_repo(db_path) == str(repo.resolve())


def test_explicit_repo_and_config(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "explicit-repo"
    other = tmp_path / "other-repo"
    repo.mkdir()
    other.mkdir()
    _init_git_repo(repo)
    _init_git_repo(other)

    custom_db = tmp_path / "custom.db"
    custom_data = tmp_path / "custom-data"
    custom_config = tmp_path / "custom.config.yaml"
    custom_config.write_text(
        CONFIG_TEMPLATE.read_text()
        .replace("PLACEHOLDER_DATA_DIR", str(custom_data))
        .replace("PLACEHOLDER_DB", str(custom_db))
    )

    result = _run_factory(
        "--repo",
        str(other),
        "--config",
        str(custom_config),
        "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    assert _session_repo(custom_db) == str(other.resolve())


def test_project_factory_config_is_selected(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    project_config_dir = repo / ".factory"
    project_config_dir.mkdir(parents=True)
    _init_git_repo(repo)

    project_db = tmp_path / "project.db"
    project_data = tmp_path / "project-data"
    project_config = project_config_dir / "sssf.config.yaml"
    project_config.write_text(
        CONFIG_TEMPLATE.read_text()
        .replace("PLACEHOLDER_DATA_DIR", str(project_data))
        .replace("PLACEHOLDER_DB", str(project_db))
    )

    result = _run_factory(
        "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    assert _session_repo(project_db) == str(repo.resolve())
    assert not Path(factory_env["FACTORY_TEST_DB"]).exists()


def test_rejects_non_git_target(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    not_repo = tmp_path / "plain-dir"
    not_repo.mkdir()

    result = _run_factory(
        "--repo",
        str(not_repo),
        "adw_session_probe",
        cwd=tmp_path,
        env=factory_env,
    )

    assert result.returncode == 2
    assert "not a git repository" in result.stderr


def test_rejects_missing_config(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)

    result = _run_factory(
        "--config",
        str(tmp_path / "missing.yaml"),
        "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 2
    assert "config not found" in result.stderr


def test_rejects_unknown_adw(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)

    result = _run_factory(
        "not-a-real-adw",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 2
    assert "unknown ADW" in result.stderr


def test_project_owned_adw_invoked_by_short_name_from_nested_cwd(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    nested = repo / "packages" / "app"
    nested.mkdir(parents=True)
    project_config_dir = repo / ".factory"
    project_config_dir.mkdir(parents=True)
    _init_git_repo(repo)

    project_db = tmp_path / "project-owned.db"
    project_data = tmp_path / "project-owned-data"
    project_config = project_config_dir / "sssf.config.yaml"
    project_config.write_text(
        CONFIG_TEMPLATE.read_text()
        .replace("PLACEHOLDER_DATA_DIR", str(project_data))
        .replace("PLACEHOLDER_DB", str(project_db))
    )

    project_probe = project_config_dir / "adw_project_probe.py"
    project_probe.write_text(PROBE_ADW.read_text())
    project_probe.chmod(0o755)

    result = _run_factory(
        "project_probe",
        cwd=nested,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    assert _session_repo(project_db) == str(repo.resolve())
    assert not Path(factory_env["FACTORY_TEST_DB"]).exists()


def test_exec_uses_single_uv_run_argv(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)

    probe_log = _install_uv_argv_probe(tmp_path, factory_env)
    result = _run_factory(
        "adw_session_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 0, result.stderr
    argv = _latest_uv_argv(probe_log)
    assert argv[:3] == ["uv", "run", "--project"]
    assert argv[3] == factory_env["FACTORY_ROOT"]
    assert argv[4] == "python"
    assert argv[5].endswith("/adw_session_probe.py")
    assert argv.count("run") == 1
    assert "uv" not in argv[4:]


def test_supervisor_notifies_when_child_is_killed_before_finish(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    """SIGKILL the ADW child: the launcher parent sends the crash message."""
    repo = tmp_path / "project"
    repo.mkdir()
    _init_git_repo(repo)
    botmaster_dir = tmp_path / "botmaster-bin"
    botmaster_dir.mkdir()
    botmaster_log = tmp_path / "botmaster.log"
    botmaster = botmaster_dir / "botmaster"
    botmaster.write_text(
        "#!/usr/bin/env bash\n"
        "printf '%s\\n' \"$*\" >> \"${FACTORY_BOTMASTER_LOG:?}\"\n"
    )
    botmaster.chmod(0o755)
    factory_env.update({
        "FACTORY_NOTIFY": "on",
        "FACTORY_BOTMASTER_LOG": str(botmaster_log),
        "FACTORY_TEST_HOLD_SECONDS": "60",
        "PATH": f"{botmaster_dir}{os.pathsep}{factory_env['PATH']}",
    })
    process = subprocess.Popen(
        [str(FACTORY_BIN), "adw_session_probe", "--hold"],
        cwd=repo,
        env={**os.environ, **factory_env},
        stdin=subprocess.PIPE,
        text=True,
    )
    db_path = Path(factory_env["FACTORY_TEST_DB"])
    child_pid: int | None = None
    deadline = time.monotonic() + 10
    while time.monotonic() < deadline:
        if db_path.exists():
            conn = sqlite3.connect(db_path)
            try:
                try:
                    row = conn.execute(
                        "SELECT pid FROM processes WHERE adw_id='probe-session' "
                        "AND kind='adw' AND ended_at IS NULL"
                    ).fetchone()
                except sqlite3.OperationalError:
                    row = None  # schema creation is still in progress
            finally:
                conn.close()
            if row:
                child_pid = int(row[0])
                break
        time.sleep(0.05)
    assert child_pid is not None, "Factory child did not record its PID"
    os.kill(child_pid, signal.SIGKILL)
    assert process.wait(timeout=10) != 0
    messages = botmaster_log.read_text().splitlines()
    assert len(messages) == 2
    assert messages[0].startswith("--channel Overdeck factory started:")
    assert messages[1] == "--channel Overdeck factory CRASHED: probe-session — ended before finishing"


def test_rejects_project_adw_symlink_escape(
    tmp_path: Path,
    factory_env: dict[str, str],
) -> None:
    repo = tmp_path / "project"
    outside = tmp_path / "outside-adw.py"
    project_config_dir = repo / ".factory"
    project_config_dir.mkdir(parents=True)
    _init_git_repo(repo)
    outside.write_text("#!/usr/bin/env python3\nprint('escaped')\n")
    outside.chmod(0o755)
    (project_config_dir / "adw_escape_probe.py").symlink_to(outside)

    result = _run_factory(
        "escape_probe",
        cwd=repo,
        env=factory_env,
    )

    assert result.returncode == 2
    assert "resolves outside" in result.stderr


def test_bootstrap_symlinks_factory_to_path(
    tmp_path: Path,
) -> None:
    harness_root = tmp_path / "harness"
    harness_root.mkdir()
    factory_root = harness_root / "factory"
    (factory_root / "bin").mkdir(parents=True)
    (factory_root / "bin" / "factory").write_text(FACTORY_BIN.read_text())
    (factory_root / "bin" / "factory").chmod(0o755)

    home = tmp_path / "home"
    home.mkdir()

    artifact = home / ".local" / "opt" / "overdeck" / "harness" / "current"
    artifact.mkdir(parents=True)
    (artifact / "factory" / "bin").mkdir(parents=True)
    (artifact / "factory" / "bin" / "factory").symlink_to(factory_root / "bin" / "factory")

    local_bin = home / ".local" / "bin"
    local_bin.mkdir(parents=True)

    env = {
        **os.environ,
        "HOME": str(home),
    }
    bootstrap = Path(__file__).resolve().parents[2] / "bootstrap.sh"
    result = subprocess.run(
        ["bash", str(bootstrap)],
        cwd=harness_root,
        env=env,
        text=True,
        capture_output=True,
    )
    assert result.returncode == 0, result.stdout + result.stderr

    link = local_bin / "factory"
    assert link.is_symlink()
    assert link.resolve() == (artifact / "factory" / "bin" / "factory").resolve()
