from __future__ import annotations

import json
import re
import subprocess
from pathlib import Path
from types import SimpleNamespace

from adw_modules import agents, gates, git_helper
from adw_modules.data_types import (AgentCall, BuildOutput, ConfigDefaults, GenericOutput,
                                    ObservabilityConfig, PhaseParams, SSSFConfig)
from adw_modules.runner import Run
from adw_modules.tracer import Tracer


def _git(repo: Path, *args: str) -> None:
    subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True)


def _run(tmp_path: Path, monkeypatch) -> Run:
    repo = tmp_path / "repo"
    repo.mkdir()
    _git(repo, "init")
    _git(repo, "config", "user.email", "test@example.com")
    _git(repo, "config", "user.name", "Test User")
    (repo / "example.txt").write_text("before\n")
    _git(repo, "add", "example.txt")
    _git(repo, "commit", "-m", "initial")
    monkeypatch.chdir(repo)
    cfg = SSSFConfig(
        defaults=ConfigDefaults(data_dir=str(tmp_path / "data")),
        observability=ObservabilityConfig(db=str(tmp_path / "sssf.db")),
    )
    tracer = Tracer(tmp_path / "sssf.db", tmp_path / "events.jsonl")
    tracer.session_start("phase_diff", "tester", repo=repo)
    return Run(cfg, "phase_diff", tracer, "tester")


def _phase(run: Run):
    return run.phase(PhaseParams(name="build", task_id="build", kind="code", owner="git",
                                 description="Modify a tracked source file."))


def test_changed_phase_writes_diff_row(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    try:
        with _phase(run):
            Path("example.txt").write_text("after\n")
        row = run.tracer.conn.execute(
            "SELECT files_json,insertions,deletions,diff_text,truncated FROM phase_diffs"
        ).fetchone()
    finally:
        run.tracer.conn.close()

    assert json.loads(row[0]) == [{"path": "example.txt", "status": "M",
                                   "insertions": 1, "deletions": 1}]
    assert row[1:3] == (1, 1)
    assert "+after" in row[3]
    assert row[4] == 0


def test_changed_agent_phase_writes_authoritative_linkage(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    params = PhaseParams(name="build", task_id="story-task", kind="agent", owner="builder",
                         description="Modify a tracked source file.")
    try:
        with run.phase(params) as handle:
            attempt_id = run.tracer.agent_attempt_start(
                run.adw_id, handle.phase.phase_id, "builder", "session-1", "pi",
                host="test-host", account=None, model="test-model",
            )
            Path("example.txt").write_text("after\n")
        row = run.tracer.conn.execute(
            "SELECT task_id,phase_id,attempt_id FROM phase_diffs"
        ).fetchone()
    finally:
        run.tracer.conn.close()

    assert handle.phase.task_id == "story-task"
    assert handle.phase.task_id != handle.phase.params.name
    assert handle.phase.task_id != handle.phase.phase_id
    assert row == (handle.phase.task_id, handle.phase.phase_id, attempt_id)


def test_phase_diff_attempt_linkage_fails_closed_when_phase_has_retries(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    params = PhaseParams(name="build", task_id="build", kind="agent", owner="builder",
                         description="Modify a tracked source file.")
    try:
        with run.phase(params) as handle:
            for session_id in ("session-1", "session-2"):
                run.tracer.agent_attempt_start(
                    run.adw_id, handle.phase.phase_id, "builder", session_id, "pi",
                    host="test-host", account=None, model="test-model",
                )
            Path("example.txt").write_text("after\n")
        row = run.tracer.conn.execute(
            "SELECT task_id,phase_id,attempt_id FROM phase_diffs"
        ).fetchone()
    finally:
        run.tracer.conn.close()

    assert row == (handle.phase.task_id, handle.phase.phase_id, None)


def test_unchanged_phase_writes_no_diff_row(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    try:
        with _phase(run):
            pass
        rows = run.tracer.conn.execute("SELECT * FROM phase_diffs").fetchall()
    finally:
        run.tracer.conn.close()

    assert rows == []


def test_long_diff_is_explicitly_truncated(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    try:
        with _phase(run):
            Path("example.txt").write_text("x" * 400_100 + "\n")
        diff_text, truncated = run.tracer.conn.execute(
            "SELECT diff_text,truncated FROM phase_diffs"
        ).fetchone()
    finally:
        run.tracer.conn.close()

    marker = diff_text[400_000:]
    assert truncated == 1
    assert len(diff_text) == 400_000 + len(marker)
    assert diff_text.endswith(marker)
    assert re.fullmatch(r"\n\[truncated: [1-9][0-9]* characters dropped\]", marker)


def test_diff_capture_failure_does_not_raise_from_phase(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    original = git_helper.phase_diff

    def fail(*_args, **_kwargs):
        raise RuntimeError("git diff failed: test failure")

    monkeypatch.setattr(git_helper, "phase_diff", fail)
    try:
        with _phase(run):
            Path("example.txt").write_text("after\n")
        row = run.tracer.conn.execute(
            "SELECT status,error FROM phases WHERE phase_id=?", (run.phases[-1].phase_id,)
        ).fetchone()
        diffs = run.tracer.conn.execute("SELECT * FROM phase_diffs").fetchall()
    finally:
        run.tracer.conn.close()
        monkeypatch.setattr(git_helper, "phase_diff", original)

    assert row == ("success", "git diff failed: test failure")
    assert diffs == []


def test_agent_call_receives_phase_start_commit(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    head = git_helper.rev()
    Path("example.txt").write_text("pre-phase\n")
    captured = {}

    def execute(_run, _phase, _call, baseline_commit):
        captured["baseline_commit"] = baseline_commit
        return GenericOutput(status="success")

    monkeypatch.setattr(agents, "execute", execute)
    try:
        params = PhaseParams(name="build", task_id="build", kind="agent", owner="builder",
                             description="Run the builder against a pinned baseline.")
        with run.phase(params) as ph:
            ph.call(AgentCall(output_type=GenericOutput, prompt="build"))
    finally:
        run.tracer.conn.close()

    baseline = captured["baseline_commit"]
    assert baseline != head
    assert subprocess.run(
        ["git", "diff", "--quiet", baseline, "--", "example.txt"], cwd=run.repo_root,
    ).returncode == 0


def test_diff_matches_claims_accepts_truthful_deletion(tmp_path: Path, monkeypatch) -> None:
    run = _run(tmp_path, monkeypatch)
    baseline = git_helper.rev()
    Path("example.txt").unlink()

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt"]),
        SimpleNamespace(repo_root=run.repo_root), baseline,
    )

    assert report.passed
    assert report.checks[0].note == "deleted in git diff"


def test_diff_matches_claims_accepts_committed_build_deletion(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    baseline = git_helper.rev()
    Path("example.txt").unlink()
    _git(run.repo_root, "add", "-A")
    _git(run.repo_root, "commit", "-m", "delete")

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt"]),
        SimpleNamespace(repo_root=run.repo_root), baseline,
    )

    assert report.passed
    assert report.checks[0].note == "deleted in git diff"


def test_diff_matches_claims_accepts_renamed_old_path(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    baseline = git_helper.rev()
    _git(run.repo_root, "mv", "example.txt", "renamed.txt")
    _git(run.repo_root, "commit", "-m", "rename")

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt"]),
        SimpleNamespace(repo_root=run.repo_root), baseline,
    )

    assert report.passed
    assert report.checks[0].note == "deleted in git diff"


def test_diff_matches_claims_rejects_pre_phase_uncommitted_deletion(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    Path("example.txt").unlink()
    baseline = git_helper.phase_baseline()

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt"]),
        SimpleNamespace(repo_root=run.repo_root), baseline,
    )

    assert report.violations == ["example.txt: claimed changed file does not exist"]


def test_diff_matches_claims_rejects_nonexistent_fabricated_claim(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["invented.txt"]),
        SimpleNamespace(repo_root=run.repo_root), git_helper.rev(),
    )

    assert report.violations == ["invented.txt: claimed changed file does not exist"]


def test_diff_matches_claims_rejects_deletion_without_trusted_baseline(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    Path("example.txt").unlink()

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt"]),
        SimpleNamespace(repo_root=run.repo_root), "",
    )

    assert report.violations == ["example.txt: trusted phase baseline unavailable"]


def test_diff_matches_claims_ignores_agent_supplied_baseline(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    baseline = git_helper.rev()
    Path("example.txt").unlink()
    envelope = BuildOutput.model_validate({
        "status": "success",
        "changed_files": ["example.txt"],
        "baseline_commit": baseline,
    })

    report = gates.diff_matches_claims(
        envelope, SimpleNamespace(repo_root=run.repo_root), "",
    )

    assert report.violations == ["example.txt: trusted phase baseline unavailable"]


def test_diff_matches_claims_keeps_existing_created_and_modified_claims(
    tmp_path: Path, monkeypatch,
) -> None:
    run = _run(tmp_path, monkeypatch)
    Path("example.txt").write_text("after\n")
    Path("created.txt").write_text("created\n")

    report = gates.diff_matches_claims(
        BuildOutput(status="success", changed_files=["example.txt", "created.txt"]),
        SimpleNamespace(repo_root=run.repo_root), git_helper.rev(),
    )

    assert report.passed
    assert [check.note for check in report.checks] == ["exists, 6B", "exists, 8B"]
