"""End-to-end: worktree lifecycle + config swap + a fake adapter + assertions,
without needing a real `claude` binary. This is the path unit tests on the
individual modules don't exercise: does the worktree the adapter sees
actually contain the config variant, and does cleanup leave the repo clean.
"""

from __future__ import annotations

from pathlib import Path

from ablate.agents.base import PreflightResult, TrialSpec, TrialTelemetry
from ablate.config.variant import ConfigVariant
from ablate.gitutil import run_git
from ablate.runner.schedule import build_jobs, run_jobs
from ablate.runner.trial import TrialJob, run_trial
from ablate.task import Assertion, Task


class FakeAdapter:
    """Records what it saw so the test can assert on it, and makes a
    deterministic edit + writes a test file so assertions have something
    to check."""

    name = "fake"

    def __init__(self):
        self.seen_specs: list[TrialSpec] = []

    def preflight(self) -> PreflightResult:
        return PreflightResult(ok=True, version="0.0.0-fake")

    def build_command(self, spec: TrialSpec) -> list[str]:
        return ["true"]

    def invoke(self, spec: TrialSpec) -> TrialTelemetry:
        self.seen_specs.append(spec)
        (spec.cwd / "tests").mkdir(exist_ok=True)
        (spec.cwd / "tests" / "test_fix.py").write_text("def test_x(): assert True\n")
        return TrialTelemetry(
            input_tokens=100, output_tokens=20, turns=2, cost_usd=0.01, exit_status=0,
        )


def _task() -> Task:
    return Task(
        id="fix-it",
        prompt="fix it",
        config_path="CLAUDE.md",
        **{
            "assert": [
                Assertion(id="regression-added", type="files_touched", must_include=["tests/**"]),
            ]
        },
    )


def test_run_trial_sees_config_variant_and_cleans_up_worktree(git_repo: Path, tmp_path: Path):
    variant = ConfigVariant(id="A", text="# custom conventions\n", source="inline")
    adapter = FakeAdapter()
    worktrees_root = tmp_path / "worktrees"

    record = run_trial(
        job=TrialJob(task=_task(), variant=variant, replicate=1),
        repo=git_repo,
        worktrees_root=worktrees_root,
        adapter=adapter,
        run_id="run1",
    )

    assert record.passed is True
    assert record.telemetry.turns == 2
    assert adapter.seen_specs[0].config_variant_text == "# custom conventions\n"

    # Worktree removed afterward, and the original repo untouched.
    assert not (worktrees_root / record.trial_id).exists()
    assert not (git_repo / "tests" / "test_fix.py").exists()


def test_run_trial_config_b_dev_null_deletes_config_in_worktree(git_repo: Path, tmp_path: Path):
    variant = ConfigVariant.from_path("B", "/dev/null")
    seen: dict[str, bool] = {}

    class CheckingAdapter(FakeAdapter):
        def invoke(self, spec: TrialSpec) -> TrialTelemetry:
            seen["config_exists"] = (spec.cwd / "CLAUDE.md").exists()
            return super().invoke(spec)

    run_trial(
        job=TrialJob(task=_task(), variant=variant, replicate=1),
        repo=git_repo,
        worktrees_root=tmp_path / "worktrees",
        adapter=CheckingAdapter(),
        run_id="run1",
    )

    assert seen["config_exists"] is False


def test_scheduler_runs_full_design_and_keeps_repo_clean(git_repo: Path, tmp_path: Path):
    variants = [
        ConfigVariant(id="A", text="# conventions\n", source="inline-A"),
        ConfigVariant.from_path("B", "/dev/null"),
    ]
    jobs = build_jobs([_task()], variants, trials=2, seed=7)
    assert len(jobs) == 1 * 2 * 2  # 1 task x 2 variants x 2 trials

    records, stopped = run_jobs(
        jobs=jobs,
        repo=git_repo,
        worktrees_root=tmp_path / "worktrees",
        adapter=FakeAdapter(),
        run_id="run1",
        concurrency=2,
    )

    assert not stopped
    assert len(records) == 4
    assert all(r.passed for r in records)
    assert {r.variant_id for r in records} == {"A", "B"}
    # Repo has no leftover worktree registrations after a full run.
    out = run_git(["worktree", "list"], cwd=git_repo).stdout
    assert out.count("\n") == 1  # just the main worktree


# --- injection modes (Q2) ---------------------------------------------------


def test_append_mode_isolates_and_injects():
    """The production path: --bare so nothing ambient leaks in, and the config
    delivered explicitly because --bare means the CLI will not read it."""
    from ablate.agents.claude_code import ClaudeCodeAdapter

    spec = TrialSpec(
        prompt="fix it",
        cwd=Path("/tmp/wt"),
        config_variant_text="# conventions\n",
        config_path="CLAUDE.md",
    )
    cmd = ClaudeCodeAdapter(injection="append").build_command(spec)

    assert "--bare" in cmd
    assert "--append-system-prompt-file" in cmd
    assert str(Path("/tmp/wt") / "CLAUDE.md") in cmd


def test_native_mode_lets_the_cli_discover_the_config():
    """The Q2 control arm: no --bare, no injection flag. The config-swap step
    has already written the variant into the worktree, so the CLI finds it the
    way a user's own session would."""
    from ablate.agents.claude_code import ClaudeCodeAdapter

    spec = TrialSpec(
        prompt="fix it",
        cwd=Path("/tmp/wt"),
        config_variant_text="# conventions\n",
        config_path="CLAUDE.md",
    )
    cmd = ClaudeCodeAdapter(injection="native").build_command(spec)

    assert "--bare" not in cmd
    assert "--append-system-prompt-file" not in cmd


def test_the_two_modes_differ_only_in_delivery():
    """Everything else must match, or the experiment measures the difference
    between two harnesses rather than between two delivery mechanisms."""
    from ablate.agents.claude_code import ClaudeCodeAdapter

    spec = TrialSpec(
        prompt="fix it", cwd=Path("/tmp/wt"), config_variant_text="# c\n", model="m"
    )
    append = ClaudeCodeAdapter(injection="append").build_command(spec)
    native = ClaudeCodeAdapter(injection="native").build_command(spec)

    delivery = {"--bare", "--append-system-prompt-file", str(Path("/tmp/wt") / "CLAUDE.md")}
    assert [c for c in append if c not in delivery] == [c for c in native if c not in delivery]


def test_append_is_the_default():
    from ablate.agents.claude_code import ClaudeCodeAdapter

    assert ClaudeCodeAdapter().injection == "append"


def test_unknown_injection_mode_is_rejected():
    import pytest

    from ablate.agents.claude_code import ClaudeCodeAdapter

    with pytest.raises(ValueError, match="injection mode"):
        ClaudeCodeAdapter(injection="telepathy")


def test_native_mode_still_writes_the_config_into_the_worktree(git_repo: Path, tmp_path: Path):
    """Native mode depends on the config being on disk. If the config-swap step
    ever stopped writing it, the Q2 control arm would silently run with no
    config at all and the experiment would look reassuring."""
    variant = ConfigVariant(id="B", text="# native conventions\n", source="inline")
    seen: dict[str, str] = {}

    class CheckingAdapter(FakeAdapter):
        def invoke(self, spec: TrialSpec) -> TrialTelemetry:
            seen["contents"] = (spec.cwd / "CLAUDE.md").read_text()
            return super().invoke(spec)

    run_trial(
        job=TrialJob(task=_task(), variant=variant, replicate=1),
        repo=git_repo,
        worktrees_root=tmp_path / "worktrees",
        adapter=CheckingAdapter(),
        run_id="run1",
    )

    assert seen["contents"] == "# native conventions\n"
