"""Unit tests for the self-modification loop.

Covers:
- Protected branch guard
- Successful cycle (mocked git + tests)
- Edit function failure → branch reverted, no commit
- Test failure → branch reverted, no commit
- Gitea PR creation plumbing
- GiteaClient graceful degradation (no token, network error)

All git and subprocess calls are mocked so these run offline without
a real repo or test suite.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_loop(repo_root="/tmp/fake-repo"):
    """Construct a SelfModifyLoop with a fake repo root."""
    from self_coding.self_modify.loop import SelfModifyLoop

    return SelfModifyLoop(repo_root=repo_root, remote="origin", base_branch="main")


def _noop_edit(repo_root: str) -> None:
    """Edit function that does nothing."""


def _failing_edit(repo_root: str) -> None:
    """Edit function that raises."""
    raise RuntimeError("edit exploded")


# ---------------------------------------------------------------------------
# Guard tests (sync — no git calls needed)
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_guard_blocks_main():
    loop = _make_loop()
    with pytest.raises(ValueError, match="protected branch"):
        loop._guard_branch("main")


@pytest.mark.unit
def test_guard_blocks_master():
    loop = _make_loop()
    with pytest.raises(ValueError, match="protected branch"):
        loop._guard_branch("master")


@pytest.mark.unit
def test_guard_allows_feature_branch():
    loop = _make_loop()
    # Should not raise
    loop._guard_branch("self-modify/some-feature")


@pytest.mark.unit
def test_guard_allows_self_modify_prefix():
    loop = _make_loop()
    loop._guard_branch("self-modify/issue-983")


# ---------------------------------------------------------------------------
# Full cycle — success path
# ---------------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.asyncio
async def test_run_success():
    """Happy path: edit succeeds, tests pass, PR created."""
    loop = _make_loop()

    fake_completed = MagicMock()
    fake_completed.stdout = "abc1234\n"
    fake_completed.returncode = 0

    fake_test_result = MagicMock()
    fake_test_result.stdout = "3 passed"
    fake_test_result.stderr = ""
    fake_test_result.returncode = 0

    from self_coding.gitea_client import PullRequest as _PR

    fake_pr = _PR(number=42, title="test PR", html_url="http://gitea/pr/42")

    with (
        patch.object(loop, "_git", return_value=fake_completed),
        patch("subprocess.run", return_value=fake_test_result),
        patch.object(loop, "_create_pr", return_value=fake_pr),
    ):
        result = await loop.run(
            slug="test-feature",
            description="Add test feature",
            edit_fn=_noop_edit,
            issue_number=983,
        )

    assert result.success is True
    assert result.branch == "self-modify/test-feature"
    assert result.pr_url == "http://gitea/pr/42"
    assert result.pr_number == 42
    assert "3 passed" in result.test_output


@pytest.mark.unit
@pytest.mark.asyncio
async def test_run_skips_tests_when_flag_set():
    """skip_tests=True should bypass the test gate."""
    loop = _make_loop()

    fake_completed = MagicMock()
    fake_completed.stdout = "deadbeef\n"
    fake_completed.returncode = 0

    with (
        patch.object(loop, "_git", return_value=fake_completed),
        patch.object(loop, "_create_pr", return_value=None),
        patch("subprocess.run") as mock_run,
    ):
        result = await loop.run(
            slug="skip-test-feature",
            description="Skip test feature",
            edit_fn=_noop_edit,
            skip_tests=True,
        )

    # subprocess.run should NOT be called for tests
    mock_run.assert_not_called()
    assert result.success is True
    assert "(tests skipped)" in result.test_output


# ---------------------------------------------------------------------------
# Failure paths
# ---------------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.asyncio
async def test_run_reverts_on_edit_failure():
    """If edit_fn raises, the branch should be reverted and no commit made."""
    loop = _make_loop()

    fake_completed = MagicMock()
    fake_completed.stdout = ""
    fake_completed.returncode = 0

    revert_called = []

    def _fake_revert(branch):
        revert_called.append(branch)

    with (
        patch.object(loop, "_git", return_value=fake_completed),
        patch.object(loop, "_revert_branch", side_effect=_fake_revert),
        patch.object(loop, "_commit_all") as mock_commit,
    ):
        result = await loop.run(
            slug="broken-edit",
            description="This will fail",
            edit_fn=_failing_edit,
            skip_tests=True,
        )

    assert result.success is False
    assert "edit exploded" in result.error
    assert "self-modify/broken-edit" in revert_called
    mock_commit.assert_not_called()


@pytest.mark.unit
@pytest.mark.asyncio
async def test_run_reverts_on_test_failure():
    """If tests fail, branch should be reverted and no commit made."""
    loop = _make_loop()

    fake_completed = MagicMock()
    fake_completed.stdout = ""
    fake_completed.returncode = 0

    fake_test_result = MagicMock()
    fake_test_result.stdout = "FAILED test_foo"
    fake_test_result.stderr = "1 failed"
    fake_test_result.returncode = 1

    revert_called = []

    def _fake_revert(branch):
        revert_called.append(branch)

    with (
        patch.object(loop, "_git", return_value=fake_completed),
        patch("subprocess.run", return_value=fake_test_result),
        patch.object(loop, "_revert_branch", side_effect=_fake_revert),
        patch.object(loop, "_commit_all") as mock_commit,
    ):
        result = await loop.run(
            slug="tests-will-fail",
            description="This will fail tests",
            edit_fn=_noop_edit,
        )

    assert result.success is False
    assert "Tests failed" in result.error
    assert "self-modify/tests-will-fail" in revert_called
    mock_commit.assert_not_called()


@pytest.mark.unit
@pytest.mark.asyncio
async def test_run_slug_with_main_creates_safe_branch():
    """A slug of 'main' produces branch 'self-modify/main', which is not protected."""

    loop = _make_loop()

    fake_completed = MagicMock()
    fake_completed.stdout = "deadbeef\n"
    fake_completed.returncode = 0

    # 'self-modify/main' is NOT in _PROTECTED_BRANCHES so the run should succeed
    with (
        patch.object(loop, "_git", return_value=fake_completed),
        patch.object(loop, "_create_pr", return_value=None),
    ):
        result = await loop.run(
            slug="main",
            description="try to write to self-modify/main",
            edit_fn=_noop_edit,
            skip_tests=True,
        )
    assert result.branch == "self-modify/main"
    assert result.success is True


# ---------------------------------------------------------------------------
# GiteaClient tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_gitea_client_returns_none_without_token():
    """GiteaClient should return None gracefully when no token is set."""
    from self_coding.gitea_client import GiteaClient

    client = GiteaClient(base_url="http://localhost:3000", token="", repo="owner/repo")
    pr = client.create_pull_request(
        title="Test PR",
        body="body",
        head="self-modify/test",
    )
    assert pr is None


@pytest.mark.unit
def test_gitea_client_comment_returns_false_without_token():
    """add_issue_comment should return False gracefully when no token is set."""
    from self_coding.gitea_client import GiteaClient

    client = GiteaClient(base_url="http://localhost:3000", token="", repo="owner/repo")
    result = client.add_issue_comment(123, "hello")
    assert result is False


@pytest.mark.unit
def test_gitea_client_create_pr_handles_network_error():
    """create_pull_request should return None on network failure."""
    from self_coding.gitea_client import GiteaClient

    client = GiteaClient(base_url="http://localhost:3000", token="fake-token", repo="owner/repo")

    mock_requests = MagicMock()
    mock_requests.post.side_effect = Exception("Connection refused")
    mock_requests.exceptions.ConnectionError = Exception

    with patch.dict("sys.modules", {"requests": mock_requests}):
        pr = client.create_pull_request(
            title="Test PR",
            body="body",
            head="self-modify/test",
        )
    assert pr is None


@pytest.mark.unit
def test_gitea_client_comment_handles_network_error():
    """add_issue_comment should return False on network failure."""
    from self_coding.gitea_client import GiteaClient

    client = GiteaClient(base_url="http://localhost:3000", token="fake-token", repo="owner/repo")

    mock_requests = MagicMock()
    mock_requests.post.side_effect = Exception("Connection refused")

    with patch.dict("sys.modules", {"requests": mock_requests}):
        result = client.add_issue_comment(456, "hello")
    assert result is False


@pytest.mark.unit
def test_gitea_client_create_pr_success():
    """create_pull_request should return a PullRequest on HTTP 201."""
    from self_coding.gitea_client import GiteaClient, PullRequest

    client = GiteaClient(base_url="http://localhost:3000", token="tok", repo="owner/repo")

    fake_resp = MagicMock()
    fake_resp.raise_for_status = MagicMock()
    fake_resp.json.return_value = {
        "number": 77,
        "title": "Test PR",
        "html_url": "http://localhost:3000/owner/repo/pulls/77",
    }

    mock_requests = MagicMock()
    mock_requests.post.return_value = fake_resp

    with patch.dict("sys.modules", {"requests": mock_requests}):
        pr = client.create_pull_request("Test PR", "body", "self-modify/feat")

    assert isinstance(pr, PullRequest)
    assert pr.number == 77
    assert pr.html_url == "http://localhost:3000/owner/repo/pulls/77"


# ---------------------------------------------------------------------------
# LoopResult dataclass
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_loop_result_defaults():
    from self_coding.self_modify.loop import LoopResult

    r = LoopResult(success=True)
    assert r.branch == ""
    assert r.commit_sha == ""
    assert r.pr_url == ""
    assert r.pr_number == 0
    assert r.test_output == ""
    assert r.error == ""
    assert r.elapsed_ms == 0.0
    assert r.metadata == {}


@pytest.mark.unit
def test_loop_result_failure():
    from self_coding.self_modify.loop import LoopResult

    r = LoopResult(success=False, error="something broke", branch="self-modify/test")
    assert r.success is False
    assert r.error == "something broke"
