"""Tests for the merge skill executor (loom.skills.merge)."""

from __future__ import annotations

import asyncio
import os
import subprocess
import textwrap
from pathlib import Path

import pytest

from loom.skills.merge import (
    ConflictInfo,
    MergeResult,
    MergeStatus,
    _get_conflicting_files,
    _is_simple_conflict,
    _run_git,
    execute_merge,
)


# ---------------------------------------------------------------------------
# Helpers: create a local git repo with branches for testing
# ---------------------------------------------------------------------------


def _git(cwd: Path, *args: str) -> str:
    """Synchronous git helper for test setup."""
    result = subprocess.run(
        ["git"] + list(args),
        cwd=str(cwd),
        capture_output=True,
        text=True,
        env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"git {' '.join(args)} failed: {result.stderr or result.stdout}"
        )
    return result.stdout.strip()


def _init_repo(tmp_path: Path) -> Path:
    """Create a git repo with an initial commit on main."""
    repo = tmp_path / "repo"
    repo.mkdir()
    _git(repo, "init", "-b", "main")
    _git(repo, "config", "user.email", "test@loom.dev")
    _git(repo, "config", "user.name", "Test")
    # Initial commit
    readme = repo / "README.md"
    readme.write_text("# Test Repo\n")
    _git(repo, "add", ".")
    _git(repo, "commit", "-m", "Initial commit")
    return repo


def _create_feature_branch(repo: Path, branch_name: str, files: dict[str, str]) -> None:
    """Create a feature branch with the given file changes."""
    _git(repo, "checkout", "-b", branch_name)
    for fname, content in files.items():
        fpath = repo / fname
        fpath.parent.mkdir(parents=True, exist_ok=True)
        fpath.write_text(content)
        _git(repo, "add", fname)
    _git(repo, "commit", "-m", f"Changes on {branch_name}")
    _git(repo, "checkout", "main")


def _modify_on_main(repo: Path, files: dict[str, str]) -> None:
    """Modify files on main (to create potential conflicts)."""
    for fname, content in files.items():
        fpath = repo / fname
        fpath.parent.mkdir(parents=True, exist_ok=True)
        fpath.write_text(content)
        _git(repo, "add", fname)
    _git(repo, "commit", "-m", "Changes on main")


# ---------------------------------------------------------------------------
# Unit tests: _run_git
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_run_git_success(tmp_path):
    """_run_git returns (0, stdout, stderr) on success."""
    repo = _init_repo(tmp_path)
    rc, stdout, stderr = await _run_git("status", cwd=repo)
    assert rc == 0
    assert "main" in stdout or "nothing to commit" in stdout


@pytest.mark.asyncio
async def test_run_git_raises_on_failure(tmp_path):
    """_run_git raises RuntimeError when check=True and command fails."""
    repo = _init_repo(tmp_path)
    with pytest.raises(RuntimeError, match="git checkout failed"):
        await _run_git("checkout", "nonexistent-branch", cwd=repo, check=True)


@pytest.mark.asyncio
async def test_run_git_no_raise_when_check_false(tmp_path):
    """_run_git returns non-zero rc without raising when check=False."""
    repo = _init_repo(tmp_path)
    rc, stdout, stderr = await _run_git(
        "checkout", "nonexistent-branch", cwd=repo, check=False
    )
    assert rc != 0


# ---------------------------------------------------------------------------
# Unit tests: _get_conflicting_files
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_conflicting_files_no_conflict(tmp_path):
    """When no merge is in progress, returns empty list."""
    repo = _init_repo(tmp_path)
    files = await _get_conflicting_files(repo)
    assert files == []


# ---------------------------------------------------------------------------
# Unit tests: _is_simple_conflict
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_is_simple_conflict_with_markers(tmp_path):
    """Files with merge conflict markers are NOT simple."""
    repo = _init_repo(tmp_path)
    conflict_file = repo / "conflict.txt"
    conflict_file.write_text(
        "<<<<<<< HEAD\nline from main\n=======\nline from branch\n>>>>>>> branch\n"
    )
    result = await _is_simple_conflict(["conflict.txt"], "branch", repo)
    assert result is False


@pytest.mark.asyncio
async def test_is_simple_conflict_deleted_file(tmp_path):
    """A file that does not exist (deleted on one side) is simple."""
    repo = _init_repo(tmp_path)
    result = await _is_simple_conflict(["deleted.txt"], "branch", repo)
    assert result is True


@pytest.mark.asyncio
async def test_is_simple_conflict_no_markers(tmp_path):
    """A file without conflict markers is simple."""
    repo = _init_repo(tmp_path)
    normal_file = repo / "normal.txt"
    normal_file.write_text("No conflict markers here\n")
    result = await _is_simple_conflict(["normal.txt"], "branch", repo)
    assert result is True


@pytest.mark.asyncio
async def test_is_simple_conflict_empty_list(tmp_path):
    """Empty file list is not resolvable."""
    repo = _init_repo(tmp_path)
    result = await _is_simple_conflict([], "branch", repo)
    assert result is False


# ---------------------------------------------------------------------------
# Unit tests: MergeResult.to_dict
# ---------------------------------------------------------------------------


def test_merge_result_to_dict_minimal():
    """MergeResult.to_dict with no conflict or sha."""
    r = MergeResult(
        status=MergeStatus.LOCK_FAILED,
        branch_name="feature-1",
        message="Lock held",
    )
    d = r.to_dict()
    assert d["status"] == "lock_failed"
    assert d["branch_name"] == "feature-1"
    assert d["message"] == "Lock held"
    assert "commit_sha" not in d
    assert "conflict" not in d


def test_merge_result_to_dict_with_conflict():
    """MergeResult.to_dict includes conflict details."""
    r = MergeResult(
        status=MergeStatus.CONFLICT,
        branch_name="feature-2",
        message="Conflicts found",
        conflict=ConflictInfo(
            conflicting_files=["a.py", "b.py"],
            auto_resolvable=False,
            details="merge conflict",
        ),
    )
    d = r.to_dict()
    assert d["status"] == "conflict"
    assert d["conflict"]["conflicting_files"] == ["a.py", "b.py"]
    assert d["conflict"]["auto_resolvable"] is False


def test_merge_result_to_dict_with_sha():
    """MergeResult.to_dict includes commit_sha when present."""
    r = MergeResult(
        status=MergeStatus.SUCCESS,
        branch_name="feature-3",
        message="Merged",
        commit_sha="abc123",
    )
    d = r.to_dict()
    assert d["commit_sha"] == "abc123"


# ---------------------------------------------------------------------------
# Integration tests: execute_merge (requires Redis from conftest)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_execute_merge_clean(redis_conn, tmp_path):
    """Happy path: merge a clean feature branch into main."""
    repo = _init_repo(tmp_path)
    _create_feature_branch(repo, "worktree-feature-1", {"feature.py": "print('hi')\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-feature-1",
        project_dir=repo,
    )

    assert result.status == MergeStatus.SUCCESS
    assert result.commit_sha
    assert "feature.py" in os.listdir(repo)
    # Verify we are back on main
    rc, branch, _ = await _run_git("rev-parse", "--abbrev-ref", "HEAD", cwd=repo)
    assert branch == "main"


@pytest.mark.asyncio
async def test_execute_merge_lock_contention(redis_conn, tmp_path):
    """When another merge holds the lock, execute_merge fails fast."""
    from loom.bus.locks import acquire_merge_lock

    repo = _init_repo(tmp_path)
    _create_feature_branch(repo, "worktree-feature-2", {"f.py": "pass\n"})

    # Pre-acquire the lock to simulate contention
    token = await acquire_merge_lock(redis_conn, ttl_seconds=60)
    assert token is not None

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-feature-2",
        project_dir=repo,
    )

    assert result.status == MergeStatus.LOCK_FAILED
    assert "another merge" in result.message.lower()


@pytest.mark.asyncio
async def test_execute_merge_conflict_complex(redis_conn, tmp_path):
    """When both sides modify the same file region, merge reports conflict."""
    repo = _init_repo(tmp_path)

    # Create a base file with multiple lines
    base_file = repo / "shared.py"
    base_file.write_text("line1\nline2\nline3\n")
    _git(repo, "add", ".")
    _git(repo, "commit", "-m", "Add shared.py")

    # Feature branch modifies the same lines
    _create_feature_branch(
        repo, "worktree-conflict", {"shared.py": "branch-line1\nbranch-line2\nbranch-line3\n"}
    )

    # Main also modifies the same lines
    _modify_on_main(repo, {"shared.py": "main-line1\nmain-line2\nmain-line3\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-conflict",
        project_dir=repo,
    )

    assert result.status == MergeStatus.CONFLICT
    assert result.conflict is not None
    assert "shared.py" in result.conflict.conflicting_files
    assert result.conflict.auto_resolvable is False


@pytest.mark.asyncio
async def test_execute_merge_nonexistent_branch(redis_conn, tmp_path):
    """Merging a branch that doesn't exist returns GIT_ERROR."""
    repo = _init_repo(tmp_path)

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-doesnt-exist",
        project_dir=repo,
    )

    assert result.status == MergeStatus.GIT_ERROR


@pytest.mark.asyncio
async def test_execute_merge_lock_released_on_success(redis_conn, tmp_path):
    """After a successful merge, the lock is released."""
    from loom.bus.locks import acquire_merge_lock

    repo = _init_repo(tmp_path)
    _create_feature_branch(repo, "worktree-release-1", {"r.py": "pass\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-release-1",
        project_dir=repo,
    )
    assert result.status == MergeStatus.SUCCESS

    # Lock should be free now
    token = await acquire_merge_lock(redis_conn, ttl_seconds=10)
    assert token is not None


@pytest.mark.asyncio
async def test_execute_merge_lock_released_on_failure(redis_conn, tmp_path):
    """After a failed merge (git error), the lock is released."""
    from loom.bus.locks import acquire_merge_lock

    repo = _init_repo(tmp_path)

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-doesnt-exist",
        project_dir=repo,
    )
    assert result.status == MergeStatus.GIT_ERROR

    # Lock should be free
    token = await acquire_merge_lock(redis_conn, ttl_seconds=10)
    assert token is not None


@pytest.mark.asyncio
async def test_execute_merge_lock_released_on_conflict(redis_conn, tmp_path):
    """After a conflict that cannot be auto-resolved, the lock is released."""
    from loom.bus.locks import acquire_merge_lock

    repo = _init_repo(tmp_path)

    base_file = repo / "shared.py"
    base_file.write_text("original\n")
    _git(repo, "add", ".")
    _git(repo, "commit", "-m", "Base file")

    _create_feature_branch(
        repo, "worktree-conflict-2", {"shared.py": "branch version\n"}
    )
    _modify_on_main(repo, {"shared.py": "main version\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-conflict-2",
        project_dir=repo,
    )
    assert result.status == MergeStatus.CONFLICT

    # Lock should be free
    token = await acquire_merge_lock(redis_conn, ttl_seconds=10)
    assert token is not None


@pytest.mark.asyncio
async def test_execute_merge_with_test_cmd_success(redis_conn, tmp_path):
    """When run_tests_cmd succeeds, merge completes normally."""
    repo = _init_repo(tmp_path)
    _create_feature_branch(repo, "worktree-test-ok", {"f.py": "pass\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-test-ok",
        project_dir=repo,
        run_tests_cmd="true",  # always succeeds
    )

    assert result.status == MergeStatus.SUCCESS


@pytest.mark.asyncio
async def test_execute_merge_with_test_cmd_failure(redis_conn, tmp_path):
    """When run_tests_cmd fails, merge is rolled back."""
    repo = _init_repo(tmp_path)
    _create_feature_branch(repo, "worktree-test-fail", {"f.py": "pass\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-test-fail",
        project_dir=repo,
        run_tests_cmd="false",  # always fails
    )

    assert result.status == MergeStatus.GIT_ERROR
    assert "tests failed" in result.message.lower()


@pytest.mark.asyncio
async def test_execute_merge_non_overlapping_files(redis_conn, tmp_path):
    """Merge with changes in different files succeeds cleanly."""
    repo = _init_repo(tmp_path)

    # Feature branch adds a new file
    _create_feature_branch(
        repo, "worktree-nonoverlap", {"new_feature.py": "def feature(): pass\n"}
    )

    # Main adds a different new file
    _modify_on_main(repo, {"other_file.py": "def other(): pass\n"})

    result = await execute_merge(
        redis_client=redis_conn,
        branch_name="worktree-nonoverlap",
        project_dir=repo,
    )

    assert result.status == MergeStatus.SUCCESS
    assert result.commit_sha
    assert "new_feature.py" in os.listdir(repo)
    assert "other_file.py" in os.listdir(repo)


# ---------------------------------------------------------------------------
# YAML loading test
# ---------------------------------------------------------------------------


def test_merge_yaml_loads():
    """The merge.yaml skill definition parses without error."""
    import yaml
    from pathlib import Path

    yaml_path = Path(__file__).parent.parent / "loom" / "skills" / "builtin" / "merge.yaml"
    data = yaml.safe_load(yaml_path.read_text())

    assert data["name"] == "merge"
    assert "branch_name" in data["inputs"]
    assert "project_dir" in data["inputs"]
    assert len(data["steps"]) >= 4
    # Verify steps have names and actions
    step_names = [s["name"] for s in data["steps"]]
    assert "acquire-merge-lock" in step_names
    assert "merge-branch" in step_names
    assert "release-lock" in step_names
