"""Integration tests for the V5 worktree API.

These tests use a real temporary git repository on disk. That is the
important safety boundary for this feature: V1 is about filesystem/index
isolation, so the API must exercise real `git worktree` behavior.
"""
from __future__ import annotations

import subprocess
import uuid
from pathlib import Path
from types import SimpleNamespace

from fastapi import HTTPException

from app.api.routes import worktrees
from app.db.models import WorkTree


def _git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
    result = subprocess.run(
        ["git", *args],
        cwd=repo,
        capture_output=True,
        text=True,
        check=False,
    )
    if check:
        assert result.returncode == 0, result.stderr
    return result


def _create_repo(client, name="Worktree Test Repo", project_root: str | None = None):
    payload = {"name": name}
    if project_root is not None:
        payload["project_root"] = project_root
    r = client.post("/api/v2/repos", json=payload)
    assert r.status_code == 201, r.text
    return r.json()["id"]


def _create_session(client, repo_id, title="worktree test"):
    r = client.post(
        f"/api/v4/chat/spaces/{repo_id}/sessions",
        json={"title": title, "provider": "openrouter", "model": "mock"},
    )
    assert r.status_code == 201, r.text
    return r.json()["id"]


def _commit_with_root(client, repo_id, session_id, project_root: Path):
    payload = {
        "repo_id": repo_id,
        "session_id": session_id,
        "message": "base",
        "summary": "base",
        "project_root": str(project_root),
    }
    r = client.post("/api/v4/chat/commit", json=payload)
    assert r.status_code == 201, r.text
    return r.json()


def _create_project_with_root(client, git_repo: Path):
    repo_id = _create_repo(client)
    session_id = _create_session(client, repo_id)
    _commit_with_root(client, repo_id, session_id, git_repo)
    return repo_id


def _create_worktree(client, space_id: str, **kwargs):
    payload = {
        "space_id": space_id,
        "agent": kwargs.pop("agent", "codex-local"),
        **kwargs,
    }
    return client.post("/api/v5/worktrees", json=payload)


def test_create_worktree_inserts_row_creates_directory_and_branch(
    client,
    tmp_path,
    monkeypatch,
):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    monkeypatch.setattr(worktrees.Path, "home", lambda: tmp_path / "home")

    r = _create_worktree(client, space_id, agent="Codex Local")

    assert r.status_code == 201, r.text
    data = r.json()
    worktree_path = Path(data["path"])
    assert worktree_path.exists()
    assert data["agent"] == "Codex Local"
    assert data["branch_name"].startswith("smriti/codex-local/")
    assert data["base_commit_sha"] == _git(git_repo, "rev-parse", "HEAD").stdout.strip()
    assert data["status"] == "active"
    assert _git(
        git_repo,
        "show-ref",
        "--verify",
        f"refs/heads/{data['branch_name']}",
    ).returncode == 0


def test_create_worktree_honors_explicit_branch_base_and_path(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    base_sha = _git(git_repo, "rev-parse", "HEAD").stdout.strip()
    target = tmp_path / "custom-worktree"

    r = _create_worktree(
        client,
        space_id,
        branch_name="feature/custom-worktree",
        base_commit_sha=base_sha,
        base_path=str(target),
    )

    assert r.status_code == 201, r.text
    data = r.json()
    assert data["branch_name"] == "feature/custom-worktree"
    assert data["base_commit_sha"] == base_sha
    assert data["path"] == str(target.resolve())
    assert target.exists()


def test_create_worktree_existing_branch_rejected_without_row_or_directory(
    client,
    tmp_path,
):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    _git(git_repo, "branch", "existing-worktree-branch")
    target = tmp_path / "should-not-exist"

    r = _create_worktree(
        client,
        space_id,
        branch_name="existing-worktree-branch",
        base_path=str(target),
    )

    assert r.status_code == 409
    assert "Branch already exists" in r.json()["detail"]
    assert not target.exists()
    assert client.get(f"/api/v5/worktrees?space_id={space_id}").json() == []


def test_create_worktree_uses_canonical_project_root(client, tmp_path):
    canonical_repo = _init_git_repo(tmp_path, name="canonical-project")
    fallback_repo = _init_git_repo(tmp_path, name="fallback-project")
    repo_id = _create_repo(
        client,
        name="Canonical Root Repo",
        project_root=str(canonical_repo),
    )
    session_id = _create_session(client, repo_id)
    _commit_with_root(client, repo_id, session_id, fallback_repo)
    target = tmp_path / "canonical-worktree"

    r = _create_worktree(
        client,
        repo_id,
        branch_name="feature/canonical-root",
        base_path=str(target),
    )

    assert r.status_code == 201, r.text
    assert target.exists()
    assert (
        _git(canonical_repo, "show-ref", "--verify", "refs/heads/feature/canonical-root")
        .returncode
        == 0
    )
    assert (
        _git(
            fallback_repo,
            "show-ref",
            "--verify",
            "refs/heads/feature/canonical-root",
            check=False,
        ).returncode
        != 0
    )


def test_create_worktree_lazy_backfills_project_root_from_commit(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    repo_id = _create_project_with_root(client, git_repo)
    assert client.get(f"/api/v2/repos/{repo_id}").json()["project_root"] is None

    r = _create_worktree(
        client,
        repo_id,
        branch_name="feature/lazy-backfill",
        base_path=str(tmp_path / "lazy-backfill-worktree"),
    )

    assert r.status_code == 201, r.text
    assert client.get(f"/api/v2/repos/{repo_id}").json()["project_root"] == str(
        git_repo.resolve()
    )


def test_list_show_and_close_clean_worktree(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    target = tmp_path / "list-show-close"
    created = _create_worktree(client, space_id, base_path=str(target)).json()

    list_r = client.get(f"/api/v5/worktrees?space_id={space_id}")
    assert list_r.status_code == 200
    assert [w["id"] for w in list_r.json()] == [created["id"]]

    show_r = client.get(f"/api/v5/worktrees/{created['id']}")
    assert show_r.status_code == 200
    assert show_r.json()["path"] == str(target.resolve())

    close_r = client.delete(f"/api/v5/worktrees/{created['id']}")
    assert close_r.status_code == 200, close_r.text
    closed = close_r.json()
    assert closed["status"] == "closed"
    assert closed["closed_at"] is not None
    assert not target.exists()

    active_r = client.get(f"/api/v5/worktrees?space_id={space_id}")
    assert active_r.status_code == 200
    assert active_r.json() == []

    all_r = client.get(f"/api/v5/worktrees?space_id={space_id}&include_closed=true")
    assert len(all_r.json()) == 1


def test_short_id_resolution_unique_prefix(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    created = _create_worktree(
        client,
        space_id,
        base_path=str(tmp_path / "short-id-show"),
    ).json()

    r = client.get(f"/api/v5/worktrees/{created['id'][:8]}")

    assert r.status_code == 200, r.text
    assert r.json()["id"] == created["id"]


def test_short_id_resolution_full_uuid_still_works(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    created = _create_worktree(
        client,
        space_id,
        base_path=str(tmp_path / "full-id-show"),
    ).json()

    r = client.get(f"/api/v5/worktrees/{created['id']}")

    assert r.status_code == 200, r.text
    assert r.json()["id"] == created["id"]


def test_short_id_resolution_no_match_returns_404(client):
    r = client.get("/api/v5/worktrees/deadbeef")

    assert r.status_code == 404
    assert "No worktree matches prefix" in r.json()["detail"]


def test_short_id_resolution_ambiguous_returns_422(client, db_session):
    repo_id = _create_repo(client)
    first = WorkTree(
        id=uuid.UUID("abcd1111-0000-4000-8000-000000000001"),
        repo_id=uuid.UUID(repo_id),
        agent="codex-local",
        path="/tmp/wt-one",
        branch_name="feature/one",
        status="active",
    )
    second = WorkTree(
        id=uuid.UUID("abcd2222-0000-4000-8000-000000000002"),
        repo_id=uuid.UUID(repo_id),
        agent="claude-code",
        path="/tmp/wt-two",
        branch_name="feature/two",
        status="active",
    )
    db_session.add_all([first, second])
    db_session.commit()

    r = client.get("/api/v5/worktrees/abcd")

    assert r.status_code == 422
    assert "ambiguous" in r.json()["detail"]
    assert "abcd1111" in r.json()["detail"]
    assert "abcd2222" in r.json()["detail"]


def test_short_id_resolution_too_short_returns_400(client):
    r = client.get("/api/v5/worktrees/abc")

    assert r.status_code == 400
    assert "at least 4 characters" in r.json()["detail"]


def test_short_id_resolution_close_path(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    target = tmp_path / "short-id-close"
    created = _create_worktree(client, space_id, base_path=str(target)).json()

    r = client.delete(f"/api/v5/worktrees/{created['id'][:8]}")

    assert r.status_code == 200, r.text
    assert r.json()["id"] == created["id"]
    assert r.json()["status"] == "closed"
    assert not target.exists()


def test_list_includes_probe_data_for_active_worktrees(client, tmp_path, monkeypatch):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    target = tmp_path / "probe-worktree"
    created = _create_worktree(client, space_id, base_path=str(target)).json()

    def fake_probe(worktree_id, path, branch):
        assert worktree_id == created["id"]
        assert path == str(target.resolve())
        assert branch == created["branch_name"]
        return {
            "id": worktree_id,
            "path": path,
            "branch": branch,
            "dirty_files": 3,
            "dirty_paths": ["cli/main.py", "backend/app/api/routes/worktrees.py"],
            "ahead": 1,
            "behind": 0,
            "last_commit_sha": "abc1234",
            "last_commit_relative": "5 minutes ago",
        }

    monkeypatch.setattr(worktrees, "_probe_worktree", fake_probe)

    r = client.get(f"/api/v5/worktrees?space_id={space_id}")

    assert r.status_code == 200, r.text
    data = r.json()
    assert data[0]["id"] == created["id"]
    assert data[0]["probe"] == {
        "dirty_files": 3,
        "dirty_paths": ["cli/main.py", "backend/app/api/routes/worktrees.py"],
        "ahead": 1,
        "behind": 0,
        "last_commit_sha": "abc1234",
        "last_commit_relative": "5 minutes ago",
    }


def test_list_probe_null_for_closed_worktrees(client, tmp_path, monkeypatch):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    created = _create_worktree(
        client,
        space_id,
        base_path=str(tmp_path / "closed-probe"),
    ).json()
    close_r = client.delete(f"/api/v5/worktrees/{created['id']}")
    assert close_r.status_code == 200, close_r.text

    def fail_probe(worktree_id, path, branch):
        raise AssertionError("closed worktrees should not be probed")

    monkeypatch.setattr(worktrees, "_probe_worktree", fail_probe)

    r = client.get(f"/api/v5/worktrees?space_id={space_id}&include_closed=true")

    assert r.status_code == 200, r.text
    assert r.json()[0]["id"] == created["id"]
    assert r.json()[0]["probe"] is None


def test_show_nonexistent_worktree_returns_404(client):
    r = client.get(f"/api/v5/worktrees/{uuid.uuid4()}")
    assert r.status_code == 404


def test_close_dirty_worktree_requires_force(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    target = tmp_path / "dirty-worktree"
    created = _create_worktree(client, space_id, base_path=str(target)).json()
    (target / "dirty.txt").write_text("uncommitted\n")

    r = client.delete(f"/api/v5/worktrees/{created['id']}")

    assert r.status_code == 409
    assert "uncommitted changes" in r.json()["detail"]
    assert target.exists()

    force_r = client.delete(f"/api/v5/worktrees/{created['id']}?force=true")
    assert force_r.status_code == 200, force_r.text
    assert force_r.json()["status"] == "closed"
    assert not target.exists()


def test_close_already_closed_returns_409(client, tmp_path):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    created = _create_worktree(
        client,
        space_id,
        base_path=str(tmp_path / "already-closed"),
    ).json()
    first = client.delete(f"/api/v5/worktrees/{created['id']}")
    assert first.status_code == 200

    second = client.delete(f"/api/v5/worktrees/{created['id']}")

    assert second.status_code == 409
    assert "already closed" in second.json()["detail"]


def test_create_worktree_requires_project_root(client, tmp_path):
    space_id = _create_repo(client)
    target = tmp_path / "no-root-worktree"

    r = _create_worktree(client, space_id, base_path=str(target))

    assert r.status_code == 400
    assert "project_root" in r.json()["detail"]
    assert "set-project-root" in r.json()["detail"]
    assert not target.exists()


def test_git_infrastructure_error_does_not_write_row_or_leave_target(
    client,
    tmp_path,
    monkeypatch,
):
    git_repo = _init_git_repo(tmp_path)
    space_id = _create_project_with_root(client, git_repo)
    target = tmp_path / "git-missing"
    head_sha = _git(git_repo, "rev-parse", "HEAD").stdout.strip()

    def fake_run_git(args, *, cwd=None, timeout=30.0):
        if args[0] == "show-ref":
            return SimpleNamespace(returncode=1, stdout="", stderr="")
        if args[0] == "rev-parse":
            return SimpleNamespace(returncode=0, stdout=f"{head_sha}\n", stderr="")
        if args[:2] == ["worktree", "add"]:
            raise HTTPException(status_code=500, detail="git executable not found")
        raise AssertionError(f"unexpected git call: {args}")

    monkeypatch.setattr(worktrees, "_run_git", fake_run_git)

    r = _create_worktree(client, space_id, base_path=str(target))

    assert r.status_code == 500
    assert "git executable not found" in r.json()["detail"]
    assert not target.exists()
    assert client.get(f"/api/v5/worktrees?space_id={space_id}").json() == []


def _init_git_repo(tmp_path: Path, name: str = "project") -> Path:
    repo = tmp_path / name
    repo.mkdir()
    _git(repo, "init")
    _git(repo, "config", "user.email", "test@example.com")
    _git(repo, "config", "user.name", "Test User")
    (repo / "README.md").write_text("hello\n")
    _git(repo, "add", "README.md")
    _git(repo, "commit", "-m", "initial")
    return repo
