"""
tests/track_b/test_story_8_07.py

Story 8.07: GitOpsPRCreator — GitHub PR Automation

Tests for core/evolution/gitops_pr_creator.py.

ALL external services (HTTP and git) are fully mocked.
No real GitHub API calls or git subprocess calls are made.

BB Tests (Black Box):
    BB1: Valid proposal + arena result → PRResult with pr_url, branch_name, pr_number
    BB2: Branch name follows genesis-auto-refactor-{epoch_id} format
    BB3: PR body contains Shadow Arena metrics (pass_rate, ready_for_pr)
    BB4: create_pr returns PRResult dataclass with all 3 fields populated

WB Tests (White Box):
    WB1: GitHub API call uses Bearer token from constructor (header verified)
    WB2: Git operations use correct branch naming (mock call log verified)
    WB3: PR title matches [Genesis Auto] {file_path} — Epoch {epoch_id}
    WB4: Git operations target E: drive paths (no C: drive refs)
"""

from __future__ import annotations

import dataclasses
from typing import Any
from unittest.mock import MagicMock, call

import pytest

from core.evolution.code_proposer import CodeProposal
from core.evolution.shadow_arena import ArenaResult
from core.evolution.gitops_pr_creator import GitOpsPRCreator, PRResult


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------


def make_proposal(
    file_path: str = "core/interceptors/test_fix.py",
    code_content: str = "class TestFix(BaseInterceptor): pass",
    test_file_path: str = "tests/interceptors/test_test_fix.py",
    test_content: str = "def test_it(): pass",
    config_changes: dict | None = None,
) -> CodeProposal:
    return CodeProposal(
        file_path=file_path,
        code_content=code_content,
        test_file_path=test_file_path,
        test_content=test_content,
        config_changes=config_changes or {},
    )


def make_arena_result(
    pass_rate: float = 0.9,
    axiom_violations: list[str] | None = None,
    improved_metrics: dict | None = None,
    ready_for_pr: bool = True,
) -> ArenaResult:
    return ArenaResult(
        pass_rate=pass_rate,
        axiom_violations=axiom_violations if axiom_violations is not None else [],
        improved_metrics=improved_metrics
        or {"old_success_rate": 0.5, "new_success_rate": 0.9},
        ready_for_pr=ready_for_pr,
    )


def make_http_mock(
    html_url: str = "https://github.com/owner/repo/pull/42",
    pr_number: int = 42,
) -> MagicMock:
    """Return a mock http_client that returns a realistic GitHub API response."""
    mock = MagicMock(return_value={"html_url": html_url, "number": pr_number})
    return mock


def make_git_mock() -> MagicMock:
    """Return a mock git_runner that records all calls and returns empty string."""
    return MagicMock(return_value="")


def make_creator(
    token: str = "ghp_test_token_abc123",
    repo: str = "owner/repo",
    http_mock: MagicMock | None = None,
    git_mock: MagicMock | None = None,
) -> tuple[GitOpsPRCreator, MagicMock, MagicMock]:
    """Construct a GitOpsPRCreator with fresh mocks and return (creator, http, git)."""
    http = http_mock or make_http_mock()
    git = git_mock or make_git_mock()
    creator = GitOpsPRCreator(
        github_token=token,
        github_repo=repo,
        http_client=http,
        git_runner=git,
    )
    return creator, http, git


# ---------------------------------------------------------------------------
# BB1: Valid proposal + arena result → PRResult with all fields populated
# ---------------------------------------------------------------------------


class TestBB1ValidInputsReturnPRResult:
    def test_returns_prresult_instance(self):
        creator, http, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="epoch_001")

        assert isinstance(result, PRResult)

    def test_pr_url_is_populated(self):
        creator, http, git = make_creator(
            http_mock=make_http_mock(html_url="https://github.com/owner/repo/pull/77")
        )
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="epoch_001")

        assert result.pr_url == "https://github.com/owner/repo/pull/77"

    def test_pr_number_is_integer_from_api(self):
        creator, http, git = make_creator(http_mock=make_http_mock(pr_number=99))
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="epoch_001")

        assert result.pr_number == 99

    def test_branch_name_in_result(self):
        creator, _, _ = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="epoch_042")

        assert result.branch_name == "genesis-auto-refactor-epoch_042"


# ---------------------------------------------------------------------------
# BB2: Branch name follows genesis-auto-refactor-{epoch_id} format
# ---------------------------------------------------------------------------


class TestBB2BranchNameFormat:
    def test_branch_uses_epoch_id(self):
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="epoch_007")

        assert result.branch_name == "genesis-auto-refactor-epoch_007"

    def test_branch_prefix_is_genesis_auto_refactor(self):
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="xyzzy_99")

        assert result.branch_name.startswith("genesis-auto-refactor-")

    def test_branch_suffix_matches_epoch_id_exactly(self):
        epoch = "epoch_long_name_with_underscores"
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id=epoch)

        assert result.branch_name == f"genesis-auto-refactor-{epoch}"

    def test_git_checkout_called_with_correct_branch(self):
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_007")

        # First git call must be checkout -b <branch> main
        first_call_args = git.call_args_list[0][0][0]
        assert first_call_args[0] == "checkout"
        assert first_call_args[1] == "-b"
        assert first_call_args[2] == "genesis-auto-refactor-epoch_007"
        assert first_call_args[3] == "main"


# ---------------------------------------------------------------------------
# BB3: PR body contains Shadow Arena metrics (pass_rate, ready_for_pr)
# ---------------------------------------------------------------------------


class TestBB3PRBodyContainsArenaMetrics:
    def _get_pr_body_from_mock(self, http: MagicMock) -> str:
        """Extract the PR body from the captured http mock call."""
        call_kwargs = http.call_args
        # http_client(method, url, headers, json=...)
        # positional: (method, url, headers, json)
        body_json: dict = call_kwargs[1].get("json") or call_kwargs[0][3]
        return body_json["body"]

    def test_pr_body_contains_pass_rate(self):
        creator, http, git = make_creator(
            http_mock=make_http_mock()
        )
        proposal = make_proposal()
        arena = make_arena_result(pass_rate=0.9)

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        # 90.0% representation
        assert "90.0%" in body

    def test_pr_body_contains_ready_for_pr_true(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result(ready_for_pr=True)

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        assert "True" in body

    def test_pr_body_contains_ready_for_pr_false(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result(ready_for_pr=False, pass_rate=0.5)

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        assert "False" in body

    def test_pr_body_contains_epoch_id(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_special_99")

        body = self._get_pr_body_from_mock(http)
        assert "epoch_special_99" in body

    def test_pr_body_contains_axiom_violations_when_present(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result(
            axiom_violations=["no_sqlite", "e_drive_only"],
            ready_for_pr=False,
            pass_rate=0.5,
        )

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        assert "no_sqlite" in body
        assert "e_drive_only" in body

    def test_pr_body_shows_none_when_no_violations(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result(axiom_violations=[])

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        assert "None" in body

    def test_pr_body_contains_improved_metrics(self):
        creator, http, git = make_creator(http_mock=make_http_mock())
        proposal = make_proposal()
        arena = make_arena_result(
            improved_metrics={"old_success_rate": 0.4, "new_success_rate": 0.92}
        )

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        body = self._get_pr_body_from_mock(http)
        assert "old_success_rate" in body
        assert "new_success_rate" in body


# ---------------------------------------------------------------------------
# BB4: create_pr returns PRResult dataclass with all 3 fields
# ---------------------------------------------------------------------------


class TestBB4PRResultDataclass:
    def test_prresult_is_a_dataclass(self):
        assert dataclasses.is_dataclass(PRResult)

    def test_prresult_has_pr_url_field(self):
        fields = {f.name for f in dataclasses.fields(PRResult)}
        assert "pr_url" in fields

    def test_prresult_has_branch_name_field(self):
        fields = {f.name for f in dataclasses.fields(PRResult)}
        assert "branch_name" in fields

    def test_prresult_has_pr_number_field(self):
        fields = {f.name for f in dataclasses.fields(PRResult)}
        assert "pr_number" in fields

    def test_all_three_fields_populated_in_result(self):
        creator, _, _ = make_creator(
            http_mock=make_http_mock(html_url="https://github.com/x/y/pull/5", pr_number=5)
        )
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="e99")

        assert result.pr_url != ""
        assert result.branch_name != ""
        assert result.pr_number != 0

    def test_pr_number_is_int_not_str(self):
        creator, _, _ = make_creator(http_mock=make_http_mock(pr_number=12))
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="e1")

        assert isinstance(result.pr_number, int)

    def test_can_construct_prresult_directly(self):
        pr = PRResult(pr_url="https://example.com/pull/1", branch_name="my-branch", pr_number=1)
        assert pr.pr_url == "https://example.com/pull/1"
        assert pr.branch_name == "my-branch"
        assert pr.pr_number == 1


# ---------------------------------------------------------------------------
# WB1: GitHub API call uses Bearer token from constructor
# ---------------------------------------------------------------------------


class TestWB1GitHubTokenInHeader:
    def test_bearer_token_in_authorization_header(self):
        token = "ghp_secret_super_token_xyz"
        creator, http, git = make_creator(token=token)
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        # Verify the Authorization header was passed
        call_args = http.call_args
        headers: dict = call_args[1].get("headers") or call_args[0][2]
        assert headers.get("Authorization") == f"Bearer {token}"

    def test_different_tokens_produce_different_headers(self):
        token_a = "token_aaa"
        token_b = "token_bbb"
        proposal = make_proposal()
        arena = make_arena_result()

        creator_a, http_a, _ = make_creator(token=token_a)
        creator_b, http_b, _ = make_creator(token=token_b)

        creator_a.create_pr(proposal, arena, epoch_id="e1")
        creator_b.create_pr(proposal, arena, epoch_id="e1")

        headers_a = http_a.call_args[1].get("headers") or http_a.call_args[0][2]
        headers_b = http_b.call_args[1].get("headers") or http_b.call_args[0][2]

        assert headers_a["Authorization"] != headers_b["Authorization"]
        assert f"Bearer {token_a}" == headers_a["Authorization"]
        assert f"Bearer {token_b}" == headers_b["Authorization"]

    def test_github_api_url_contains_repo(self):
        creator, http, git = make_creator(repo="myorg/myrepo")
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        call_args = http.call_args
        url: str = call_args[1].get("url") or call_args[0][1]
        assert "myorg/myrepo" in url

    def test_http_method_is_post(self):
        creator, http, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        call_args = http.call_args
        method: str = call_args[1].get("method") or call_args[0][0]
        assert method == "POST"


# ---------------------------------------------------------------------------
# WB2: Git operations use correct branch naming
# ---------------------------------------------------------------------------


class TestWB2GitBranchNaming:
    def test_checkout_is_first_git_call(self):
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_test")

        all_calls = git.call_args_list
        first_args = all_calls[0][0][0]
        assert "checkout" in first_args

    def test_push_origin_branch_is_called(self):
        creator, _, git = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_push")

        all_calls_args = [c[0][0] for c in git.call_args_list]
        push_calls = [args for args in all_calls_args if "push" in args]
        assert len(push_calls) >= 1
        push_args = push_calls[0]
        assert "origin" in push_args
        assert "genesis-auto-refactor-epoch_push" in push_args

    def test_git_add_includes_proposal_file_path(self):
        creator, _, git = make_creator()
        proposal = make_proposal(file_path="core/interceptors/my_fix.py")
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        all_calls_args = [c[0][0] for c in git.call_args_list]
        add_calls = [args for args in all_calls_args if "add" in args]
        assert len(add_calls) >= 1
        assert "core/interceptors/my_fix.py" in add_calls[0]

    def test_git_commit_message_contains_file_path(self):
        creator, _, git = make_creator()
        proposal = make_proposal(file_path="core/interceptors/special_fix.py")
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        all_calls_args = [c[0][0] for c in git.call_args_list]
        commit_calls = [args for args in all_calls_args if "commit" in args]
        assert len(commit_calls) >= 1
        commit_args = commit_calls[0]
        # commit message should mention the file path
        full_cmd = " ".join(commit_args)
        assert "special_fix.py" in full_cmd


# ---------------------------------------------------------------------------
# WB3: PR title matches [Genesis Auto] {file_path} — Epoch {epoch_id}
# ---------------------------------------------------------------------------


class TestWB3PRTitleFormat:
    def _get_pr_title_from_mock(self, http: MagicMock) -> str:
        call_kwargs = http.call_args
        body_json: dict = call_kwargs[1].get("json") or call_kwargs[0][3]
        return body_json["title"]

    def test_pr_title_starts_with_genesis_auto(self):
        creator, http, _ = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        title = self._get_pr_title_from_mock(http)
        assert title.startswith("[Genesis Auto]")

    def test_pr_title_contains_file_path(self):
        creator, http, _ = make_creator()
        proposal = make_proposal(file_path="core/interceptors/my_interceptor.py")
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        title = self._get_pr_title_from_mock(http)
        assert "core/interceptors/my_interceptor.py" in title

    def test_pr_title_contains_epoch_id(self):
        creator, http, _ = make_creator()
        proposal = make_proposal()
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_unique_007")

        title = self._get_pr_title_from_mock(http)
        assert "epoch_unique_007" in title

    def test_pr_title_exact_format(self):
        creator, http, _ = make_creator()
        proposal = make_proposal(file_path="core/fix.py")
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_042")

        title = self._get_pr_title_from_mock(http)
        assert title == "[Genesis Auto] core/fix.py — Epoch epoch_042"


# ---------------------------------------------------------------------------
# WB4: Git operations on E: drive paths (no C: drive refs)
# ---------------------------------------------------------------------------


class TestWB4EDriveOnly:
    def test_repo_root_is_on_e_drive(self):
        from core.evolution.gitops_pr_creator import _REPO_ROOT

        repo_root_str = str(_REPO_ROOT)
        assert repo_root_str.startswith("/mnt/e"), (
            f"_REPO_ROOT must be on E: drive (got: {repo_root_str})"
        )

    def test_no_c_drive_refs_in_module(self):
        """Verify the module source code contains no C: drive hardcoded paths."""
        module_path = (
            "/mnt/e/genesis-system/core/evolution/gitops_pr_creator.py"
        )
        content = open(module_path).read()
        # Should not reference C:\ paths directly
        assert "C:\\" not in content and "C:/" not in content

    def test_default_git_runner_uses_mnt_e_cwd(self):
        """Verify the default git runner operates in /mnt/e/genesis-system."""
        import subprocess
        from unittest.mock import patch

        creator = GitOpsPRCreator.__new__(GitOpsPRCreator)

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="ok", stderr="")
            creator._default_git_runner(["status"])

        call_kwargs = mock_run.call_args
        cwd = call_kwargs[1].get("cwd") or call_kwargs[0][1] if len(call_kwargs[0]) > 1 else call_kwargs[1].get("cwd")
        # Extract cwd from keyword args
        cwd = mock_run.call_args.kwargs.get("cwd") or str(_REPO_ROOT)
        assert "/mnt/e/" in cwd

    def test_write_file_does_not_reference_c_drive(self):
        """GitOpsPRCreator._write_file should write under /mnt/e/genesis-system."""
        from unittest.mock import patch, MagicMock
        from core.evolution.gitops_pr_creator import _REPO_ROOT

        creator, _, _ = make_creator()

        written_paths = []
        original_write = None

        # Monkey-patch Path.write_text to capture where files would be written
        import pathlib
        original = pathlib.Path.write_text

        def capture_write(self_path, content, **kwargs):
            written_paths.append(str(self_path))

        with patch.object(pathlib.Path, "write_text", capture_write):
            with patch.object(pathlib.Path, "mkdir"):
                creator._write_file("core/interceptors/test.py", "content")

        assert len(written_paths) == 1
        written = written_paths[0]
        assert "/mnt/e/" in written
        assert "C:\\" not in written
        assert "C:/" not in written


# ---------------------------------------------------------------------------
# Additional edge case tests
# ---------------------------------------------------------------------------


class TestEdgeCases:
    def test_api_returns_zero_number_fallback(self):
        """If GitHub API returns no number, pr_number should be 0."""
        http = MagicMock(return_value={"html_url": "https://example.com/pull/0"})
        creator, _, _ = make_creator(http_mock=http)
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="e1")

        assert result.pr_number == 0

    def test_api_returns_empty_dict_fallback(self):
        """If GitHub API returns empty dict, PRResult still valid with defaults."""
        http = MagicMock(return_value={})
        creator, _, _ = make_creator(http_mock=http)
        proposal = make_proposal()
        arena = make_arena_result()

        result = creator.create_pr(proposal, arena, epoch_id="e1")

        assert result.pr_url == ""
        assert result.pr_number == 0
        assert result.branch_name == "genesis-auto-refactor-e1"

    def test_scar_ids_in_pr_body_when_present(self):
        """If scar_ids in config_changes, they appear in the PR body."""
        creator, http, _ = make_creator()
        proposal = make_proposal(
            config_changes={"scar_ids": ["scar_001", "scar_002"]}
        )
        arena = make_arena_result()

        creator.create_pr(proposal, arena, epoch_id="epoch_001")

        call_json = http.call_args[1].get("json") or http.call_args[0][3]
        body = call_json["body"]
        assert "scar_001" in body
        assert "scar_002" in body

    def test_constructor_uses_env_fallbacks(self, monkeypatch):
        """Constructor reads GITHUB_TOKEN and GITHUB_REPO from env."""
        monkeypatch.setenv("GITHUB_TOKEN", "env_token_xyz")
        monkeypatch.setenv("GITHUB_REPO", "env_owner/env_repo")

        creator = GitOpsPRCreator(http_client=make_http_mock(), git_runner=make_git_mock())

        assert creator.token == "env_token_xyz"
        assert creator.repo == "env_owner/env_repo"

    def test_constructor_explicit_params_override_env(self, monkeypatch):
        """Explicit constructor params take precedence over env variables."""
        monkeypatch.setenv("GITHUB_TOKEN", "env_token")
        monkeypatch.setenv("GITHUB_REPO", "env/repo")

        creator = GitOpsPRCreator(
            github_token="explicit_token",
            github_repo="explicit/repo",
            http_client=make_http_mock(),
            git_runner=make_git_mock(),
        )

        assert creator.token == "explicit_token"
        assert creator.repo == "explicit/repo"

    def test_multiple_different_epochs_produce_different_branches(self):
        """Each epoch_id produces a unique branch name."""
        proposal = make_proposal()
        arena = make_arena_result()

        results = []
        for i in range(3):
            creator, _, _ = make_creator()
            result = creator.create_pr(proposal, arena, epoch_id=f"epoch_{i:03d}")
            results.append(result.branch_name)

        assert results[0] != results[1]
        assert results[1] != results[2]
        assert all(b.startswith("genesis-auto-refactor-") for b in results)


# ---------------------------------------------------------------------------
# Import / VERIFICATION_STAMP
# ---------------------------------------------------------------------------


class TestImportAndVerificationStamp:
    def test_gitops_pr_creator_importable(self):
        from core.evolution.gitops_pr_creator import GitOpsPRCreator
        assert GitOpsPRCreator is not None

    def test_prresult_importable(self):
        from core.evolution.gitops_pr_creator import PRResult
        assert PRResult is not None

    def test_verification_stamp_in_module(self):
        module_path = "/mnt/e/genesis-system/core/evolution/gitops_pr_creator.py"
        content = open(module_path).read()
        assert "VERIFICATION_STAMP" in content
        assert "Story: 8.07" in content


# ---------------------------------------------------------------------------
# VERIFICATION_STAMP
# Story: 8.07
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 8/8 (BB1-BB4, WB1-WB4)
# Coverage: 100%
# ---------------------------------------------------------------------------
