"""End-to-end mechanics for the canonical SDLC ADW."""

from __future__ import annotations

import json
import sqlite3
import subprocess
import sys
from pathlib import Path

import pytest
import yaml

import adw_sdlc
from adw_modules import agents
from adw_modules.data_types import (
    BuildOutput,
    ConfigDefaults,
    DocumentOutput,
    ObservabilityConfig,
    PhaseParams,
    PlanOutput,
    QualityCheckConfig,
    ReviewOutput,
    ScoutFinding,
    ScoutOutput,
    SSSFConfig,
)
from adw_modules.runner import Run
from adw_modules.tracer import Tracer


def _git(repo: Path, *args: str) -> str:
    result = subprocess.run(
        ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True,
    )
    return result.stdout.strip()


def _repo(tmp_path: Path) -> Path:
    repo = tmp_path / "repo"
    repo.mkdir()
    _git(repo, "init", "-b", "main")
    _git(repo, "config", "user.email", "factory@test")
    _git(repo, "config", "user.name", "Factory Test")
    (repo / "base.txt").write_text("base\n")
    _git(repo, "add", "base.txt")
    _git(repo, "commit", "-m", "base")
    _git(repo, "checkout", "-b", "factory/test")
    return repo


def _run(tmp_path: Path, checks: dict[str, QualityCheckConfig]) -> Run:
    repo = _repo(tmp_path)
    cfg = SSSFConfig(
        defaults=ConfigDefaults(data_dir=str(tmp_path / "data")),
        observability=ObservabilityConfig(db=str(tmp_path / "trace.db")),
        quality=checks,
    )
    tracer = Tracer(cfg.observability.db, tmp_path / "events.jsonl")
    tracer.session_start("canonical", "tester", repo=repo)
    run = Run(cfg, "canonical", tracer, "tester")
    run.repo_root = repo
    return run


def _python(script: str) -> list[str]:
    return [sys.executable, "-c", script]


def _build() -> BuildOutput:
    return BuildOutput(status="success", summary="build")


def test_every_configured_quality_check_runs_and_each_is_recorded(tmp_path: Path) -> None:
    marker = tmp_path / "checks.txt"
    checks = {
        "test": QualityCheckConfig(argv=_python(
            f"from pathlib import Path; Path({str(marker)!r}).open('a').write('test\\n')"
        )),
        "lint": QualityCheckConfig(operation="lint", argv=_python(
            f"from pathlib import Path; Path({str(marker)!r}).open('a').write('lint\\n')"
        )),
        "custom": QualityCheckConfig(argv=_python(
            f"from pathlib import Path; Path({str(marker)!r}).open('a').write('custom\\n')"
        )),
    }
    run = _run(tmp_path, checks)
    try:
        result, _ = adw_sdlc.run_quality_repairs(run, "do it", _build(), 0)
        rows = run.tracer.conn.execute(
            "SELECT name,payload_json FROM events WHERE type='tool_call' ORDER BY rowid"
        ).fetchall()
    finally:
        run.tracer.conn.close()

    assert result.passed
    assert marker.read_text().splitlines() == ["test", "lint", "custom"]
    assert [name for name, _ in rows] == [
        "quality:test", "quality:lint", "quality:custom",
    ]
    payloads = [json.loads(payload) for _, payload in rows]
    assert all(payload["passed"] for payload in payloads)
    assert all(Path(payload["output_artifact"]).is_file() for payload in payloads)


def test_failing_check_fails_phase_with_real_output_and_artifact(tmp_path: Path) -> None:
    run = _run(tmp_path, {
        "test": QualityCheckConfig(argv=_python(
            "import sys; print('REAL-COMMAND-FAILURE', file=sys.stderr); raise SystemExit(7)"
        )),
    })
    try:
        with pytest.raises(RuntimeError, match="REAL-COMMAND-FAILURE") as caught:
            adw_sdlc.run_quality_repairs(run, "do it", _build(), 0)
        phase = run.tracer.conn.execute(
            "SELECT status,error FROM phases WHERE name='quality_1'"
        ).fetchone()
        payload = json.loads(run.tracer.conn.execute(
            "SELECT payload_json FROM events WHERE type='tool_call'"
        ).fetchone()[0])
    finally:
        run.tracer.conn.close()

    assert phase[0] == "fail"
    assert "REAL-COMMAND-FAILURE" in phase[1]
    assert payload["returncode"] == 7
    assert payload["output_artifact"] in str(caught.value)
    assert "REAL-COMMAND-FAILURE" in Path(payload["output_artifact"]).read_text()


@pytest.mark.parametrize("fixes_problem", [True, False])
def test_repair_is_retested_and_only_a_fix_that_passes_is_accepted(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fixes_problem: bool,
) -> None:
    state = tmp_path / "state"
    attempts = tmp_path / "attempts"
    run = _run(tmp_path, {
        "test": QualityCheckConfig(argv=_python(
            "import sys; from pathlib import Path; "
            f"p=Path({str(attempts)!r}); p.open('a').write('run\\n'); "
            f"raise SystemExit(0 if Path({str(state)!r}).exists() else 9)"
        )),
    })
    repairs = 0

    def fake_execute(_run, _phase, _call, _baseline_commit):
        nonlocal repairs
        repairs += 1
        if fixes_problem:
            state.write_text("fixed\n")
        return _build()

    monkeypatch.setattr(agents, "execute", fake_execute)
    try:
        if fixes_problem:
            result, _ = adw_sdlc.run_quality_repairs(run, "do it", _build(), 1)
            assert result.passed
            assert run.finish(accepted=True) == 0
        else:
            with pytest.raises(RuntimeError):
                adw_sdlc.run_quality_repairs(run, "do it", _build(), 1)
        phases = run.tracer.conn.execute(
            "SELECT name,status FROM phases ORDER BY seq"
        ).fetchall()
    finally:
        run.tracer.conn.close()

    assert repairs == 1
    assert attempts.read_text().splitlines() == ["run", "run"]
    assert [name for name, _ in phases] == ["quality_1", "repair_1", "quality_2"]
    assert phases[0] == ("quality_1", "fail")
    assert phases[1] == ("repair_1", "success")


def test_configured_iteration_bound_is_honoured(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, {
        "test": QualityCheckConfig(argv=_python("raise SystemExit(4)")),
    })
    repairs = 0

    def fake_execute(_run, _phase, _call, _baseline_commit):
        nonlocal repairs
        repairs += 1
        return _build()

    monkeypatch.setattr(agents, "execute", fake_execute)
    try:
        with pytest.raises(RuntimeError):
            adw_sdlc.run_quality_repairs(run, "do it", _build(), 2)
        names = [row[0] for row in run.tracer.conn.execute(
            "SELECT name FROM phases ORDER BY seq"
        )]
    finally:
        run.tracer.conn.close()

    assert repairs == 2
    assert names == ["quality_1", "repair_1", "quality_2", "repair_2", "quality_3"]


def test_complete_run_phases_are_in_the_trace_in_order(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    repo = _repo(tmp_path)
    # main() owns branch creation, so return this fixture to its protected base.
    _git(repo, "checkout", "main")
    overlay = tmp_path / "config.yaml"
    db = tmp_path / "complete.db"
    data = tmp_path / "complete-data"
    overlay.write_text(yaml.safe_dump({
        "defaults": {"data_dir": str(data), "git_branch_mode": "create"},
        "observability": {"db": str(db)},
        "workflow": {"max_repair_iterations": 1, "max_revision_iterations": 1},
        "quality": {
            "test": {"argv": ["true"]},
            "lint": {"argv": ["true"], "operation": "lint"},
            "typecheck": {"argv": ["true"], "operation": "typecheck"},
            "build": {"argv": ["true"]},
        },
    }))

    agent_calls = []

    def fake_execute(run, phase, call, _baseline_commit):
        agent_calls.append((phase.params.owner, call.previous))
        if phase.params.owner == "scout":
            return ScoutOutput(
                status="success",
                summary="mapped",
                artifacts=[str(repo / "base.txt")],
                findings=[ScoutFinding(file="base.txt", note="starting point")],
            )
        if phase.params.owner == "planner":
            path = repo / "plan.md"
            path.write_text("plan\n")
            run.claim_paths(["plan.md"])
            return PlanOutput(status="success", summary="plan", artifacts=[str(path)])
        if phase.params.owner == "builder":
            path = repo / "implementation.txt"
            path.write_text("built\n")
            run.claim_paths(["implementation.txt"])
            return BuildOutput(status="success", summary="build",
                               changed_files=[str(path)], commit_message="build")
        if phase.params.owner == "reviewer":
            return ReviewOutput(status="success", summary="approved", approved=True)
        path = repo / "app_docs" / "change.md"
        path.parent.mkdir()
        path.write_text("docs\n")
        run.claim_paths(["app_docs/change.md"])
        return DocumentOutput(status="success", summary="docs", artifacts=[str(path)],
                              document_path=str(path), commit_message="docs")

    monkeypatch.setattr(agents, "execute", fake_execute)
    monkeypatch.setattr(agents, "validate", lambda _cfg, _required: None)
    monkeypatch.chdir(repo)
    assert adw_sdlc.main("build it", str(overlay), "complete") == 0

    conn = sqlite3.connect(db)
    try:
        names = [row[0] for row in conn.execute(
            "SELECT name FROM phases WHERE adw_id='complete' ORDER BY seq"
        )]
        status = conn.execute(
            "SELECT status FROM sessions WHERE adw_id='complete'"
        ).fetchone()[0]
    finally:
        conn.close()

    assert names == [
        "git_preflight", "request", "scout", "plan", "build", "quality_1", "review_1",
        "commit_build", "changes", "document", "commit_docs",
    ]
    assert [owner for owner, _ in agent_calls] == [
        "scout", "planner", "builder", "reviewer", "documenter",
    ]
    assert isinstance(agent_calls[1][1], ScoutOutput)
    assert agent_calls[1][1].findings == [
        ScoutFinding(file="base.txt", note="starting point"),
    ]
    assert status == "success"
    assert _git(repo, "log", "-2", "--format=%s").splitlines() == ["docs", "build"]
