"""Tests for FakeGitHubIssues test infrastructure.

These tests verify that FakeGitHubIssues correctly simulates GitHub issue operations,
providing reliable test doubles for tests that use issue functionality.
"""

from datetime import UTC, datetime

import pytest

from erk_shared.gateway.github.issues.fake import FakeGitHubIssues
from erk_shared.gateway.github.issues.types import IssueNotFound
from erk_shared.gateway.github.types import BodyText
from tests.test_utils.github_helpers import create_test_issue
from tests.test_utils.paths import sentinel_path


def test_fake_github_issues_initialization() -> None:
    """Test that FakeGitHubIssues initializes with empty state."""
    issues = FakeGitHubIssues()

    result = issues.list_issues(repo_root=sentinel_path())
    assert result == []


def test_fake_github_issues_create_issue_returns_number() -> None:
    """Test create_issue returns predictable issue number."""
    issues = FakeGitHubIssues(next_issue_number=42)

    result = issues.create_issue(
        repo_root=sentinel_path(),
        title="Test Issue",
        body="Test body",
        labels=["plan", "erk"],
    )

    assert result.number == 42
    assert result.url == "https://github.com/test-owner/test-repo/issues/42"


def test_fake_github_issues_create_issue_increments_number() -> None:
    """Test create_issue increments issue numbers sequentially."""
    issues = FakeGitHubIssues(next_issue_number=1)

    result1 = issues.create_issue(
        repo_root=sentinel_path(), title="Issue 1", body="Body 1", labels=["label1"]
    )
    result2 = issues.create_issue(
        repo_root=sentinel_path(), title="Issue 2", body="Body 2", labels=["label2"]
    )
    result3 = issues.create_issue(
        repo_root=sentinel_path(), title="Issue 3", body="Body 3", labels=["label3"]
    )

    assert result1.number == 1
    assert result2.number == 2
    assert result3.number == 3


def test_fake_github_issues_create_issue_tracks_mutation() -> None:
    """Test create_issue tracks created issues in mutation list."""
    issues = FakeGitHubIssues()

    issues.create_issue(
        repo_root=sentinel_path(), title="Title 1", body="Body 1", labels=["label1", "label2"]
    )
    issues.create_issue(
        repo_root=sentinel_path(), title="Title 2", body="Body 2", labels=["label3"]
    )

    assert issues.created_issues == [
        ("Title 1", "Body 1", ["label1", "label2"]),
        ("Title 2", "Body 2", ["label3"]),
    ]


def test_fake_github_issues_created_issues_empty_initially() -> None:
    """Test created_issues property is empty list initially."""
    issues = FakeGitHubIssues()

    assert issues.created_issues == []


def test_fake_github_issues_created_issues_read_only() -> None:
    """Test created_issues property returns list that can be read."""
    issues = FakeGitHubIssues()
    issues.create_issue(repo_root=sentinel_path(), title="Title", body="Body", labels=["label"])

    # Should be able to read the list
    created = issues.created_issues
    assert len(created) == 1
    assert created[0] == ("Title", "Body", ["label"])


def test_fake_github_issues_get_issue_existing() -> None:
    """Test get_issue returns stored issue for existing number."""
    pre_configured = {42: create_test_issue(42, "Existing Issue", "Existing body")}
    issues = FakeGitHubIssues(issues=pre_configured)

    result = issues.get_issue(sentinel_path(), 42)

    assert result.number == 42
    assert result.title == "Existing Issue"
    assert result.body == "Existing body"
    assert result.state == "OPEN"
    assert result.url == "https://github.com/owner/repo/issues/42"


def test_fake_github_issues_get_issue_missing() -> None:
    """Test get_issue returns IssueNotFound for missing issue."""
    issues = FakeGitHubIssues()

    result = issues.get_issue(sentinel_path(), 999)

    assert isinstance(result, IssueNotFound)
    assert result.issue_number == 999


def test_fake_github_issues_get_issue_created() -> None:
    """Test get_issue returns issue that was created via create_issue."""
    issues = FakeGitHubIssues(next_issue_number=10)

    created = issues.create_issue(
        repo_root=sentinel_path(),
        title="Created Issue",
        body="Created body",
        labels=["test"],
    )

    result = issues.get_issue(sentinel_path(), created.number)

    assert result.number == 10
    assert result.title == "Created Issue"
    assert result.body == "Created body"
    assert result.state == "OPEN"
    assert result.url == "https://github.com/test-owner/test-repo/issues/10"


def test_fake_github_issues_add_comment_existing_issue() -> None:
    """Test add_comment tracks mutation for existing issue and returns comment ID."""
    pre_configured = {42: create_test_issue(42, "Test", "Body")}
    issues = FakeGitHubIssues(issues=pre_configured)

    comment_id = issues.add_comment(sentinel_path(), 42, "This is a comment")

    assert comment_id == 1000  # First comment ID starts at 1000
    assert issues.added_comments == [(42, "This is a comment", 1000)]


def test_fake_github_issues_add_comment_missing_issue() -> None:
    """Test add_comment raises RuntimeError for missing issue."""
    issues = FakeGitHubIssues()

    with pytest.raises(RuntimeError, match="Issue #999 not found"):
        issues.add_comment(sentinel_path(), 999, "Comment body")


def test_fake_github_issues_add_comment_multiple() -> None:
    """Test add_comment tracks multiple comments in order with incrementing IDs."""
    pre_configured = {
        10: create_test_issue(10, "Issue 10", "Body", url="http://url/10"),
        20: create_test_issue(20, "Issue 20", "Body", url="http://url/20"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    id1 = issues.add_comment(sentinel_path(), 10, "First comment")
    id2 = issues.add_comment(sentinel_path(), 20, "Second comment")
    id3 = issues.add_comment(sentinel_path(), 10, "Third comment on issue 10")

    assert id1 == 1000
    assert id2 == 1001
    assert id3 == 1002
    assert issues.added_comments == [
        (10, "First comment", 1000),
        (20, "Second comment", 1001),
        (10, "Third comment on issue 10", 1002),
    ]


def test_fake_github_issues_added_comments_empty_initially() -> None:
    """Test added_comments property is empty list initially."""
    issues = FakeGitHubIssues()

    assert issues.added_comments == []


def test_fake_github_issues_update_issue_body_existing_issue() -> None:
    """Test update_issue_body updates body for existing issue."""
    pre_configured = {42: create_test_issue(42, "Test", "Original body")}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.update_issue_body(sentinel_path(), 42, BodyText(content="Updated body content"))

    # Verify body was updated
    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.body == "Updated body content"
    assert updated_issue.title == "Test"  # Title unchanged
    assert updated_issue.number == 42  # Number unchanged


def test_fake_github_issues_update_issue_body_missing_issue() -> None:
    """Test update_issue_body raises RuntimeError for missing issue."""
    issues = FakeGitHubIssues()

    with pytest.raises(RuntimeError, match="Issue #999 not found"):
        issues.update_issue_body(sentinel_path(), 999, BodyText(content="New body"))


def test_fake_github_issues_update_issue_body_updates_timestamp() -> None:
    """Test update_issue_body updates the updated_at timestamp."""
    creation_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
    pre_configured = {
        42: create_test_issue(
            42, "Test", "Original body", created_at=creation_time, updated_at=creation_time
        )
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    # Update the body
    issues.update_issue_body(sentinel_path(), 42, BodyText(content="Updated body"))

    # Verify timestamp was updated (should be later than creation time)
    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.created_at == creation_time  # Creation time unchanged
    assert updated_issue.updated_at > creation_time  # Update time advanced


def test_fake_github_issues_added_comments_read_only() -> None:
    """Test added_comments property returns list that can be read."""
    pre_configured = {42: create_test_issue(42, "Test", "Body", url="http://url")}
    issues = FakeGitHubIssues(issues=pre_configured)
    comment_id = issues.add_comment(sentinel_path(), 42, "Comment")

    # Should be able to read the list
    comments = issues.added_comments
    assert len(comments) == 1
    assert comments[0] == (42, "Comment", comment_id)


def test_fake_github_issues_list_issues_empty() -> None:
    """Test list_issues returns empty list when no issues exist."""
    issues = FakeGitHubIssues()

    result = issues.list_issues(repo_root=sentinel_path())

    assert result == []


def test_fake_github_issues_list_issues_all() -> None:
    """Test list_issues returns all issues when no filters applied."""
    pre_configured = {
        1: create_test_issue(1, "Issue 1", "Body 1", url="http://url/1"),
        2: create_test_issue(2, "Issue 2", "Body 2", "CLOSED", "http://url/2"),
        3: create_test_issue(3, "Issue 3", "Body 3", url="http://url/3"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    result = issues.list_issues(repo_root=sentinel_path())

    assert len(result) == 3
    assert result[0].number == 1
    assert result[1].number == 2
    assert result[2].number == 3


def test_fake_github_issues_list_issues_filter_open() -> None:
    """Test list_issues filters by state=open."""
    pre_configured = {
        1: create_test_issue(1, "Issue 1", "Body 1", url="http://url/1"),
        2: create_test_issue(2, "Issue 2", "Body 2", "CLOSED", "http://url/2"),
        3: create_test_issue(3, "Issue 3", "Body 3", url="http://url/3"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    result = issues.list_issues(repo_root=sentinel_path(), state="open")

    assert len(result) == 2
    assert result[0].number == 1
    assert result[0].state == "OPEN"
    assert result[1].number == 3
    assert result[1].state == "OPEN"


def test_fake_github_issues_list_issues_filter_closed() -> None:
    """Test list_issues filters by state=closed."""
    pre_configured = {
        1: create_test_issue(1, "Issue 1", "Body 1", url="http://url/1"),
        2: create_test_issue(2, "Issue 2", "Body 2", "CLOSED", "http://url/2"),
        3: create_test_issue(3, "Issue 3", "Body 3", "CLOSED", "http://url/3"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    result = issues.list_issues(repo_root=sentinel_path(), state="closed")

    assert len(result) == 2
    assert result[0].number == 2
    assert result[0].state == "CLOSED"
    assert result[1].number == 3
    assert result[1].state == "CLOSED"


def test_fake_github_issues_list_issues_state_all() -> None:
    """Test list_issues with state=all returns all issues."""
    pre_configured = {
        1: create_test_issue(1, "Issue 1", "Body 1", url="http://url/1"),
        2: create_test_issue(2, "Issue 2", "Body 2", "CLOSED", "http://url/2"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    result = issues.list_issues(repo_root=sentinel_path(), state="all")

    assert len(result) == 2


def test_fake_github_issues_list_issues_includes_created() -> None:
    """Test list_issues includes issues created via create_issue."""
    issues = FakeGitHubIssues(next_issue_number=1)

    issues.create_issue(
        repo_root=sentinel_path(), title="Created Issue", body="Body", labels=["label"]
    )

    result = issues.list_issues(repo_root=sentinel_path())

    assert len(result) == 1
    assert result[0].number == 1
    assert result[0].title == "Created Issue"
    assert result[0].state == "OPEN"


def test_fake_github_issues_full_workflow() -> None:
    """Test complete workflow: create, get, comment, list."""
    # Start with one pre-configured issue
    pre_configured = {100: create_test_issue(100, "Existing", "Body", url="http://url/100")}
    issues = FakeGitHubIssues(issues=pre_configured, next_issue_number=200)

    # Create new issue
    new_result = issues.create_issue(
        repo_root=sentinel_path(),
        title="New Issue",
        body="New body",
        labels=["plan", "erk"],
    )
    assert new_result.number == 200

    # Get created issue
    new_issue = issues.get_issue(sentinel_path(), 200)
    assert new_issue.title == "New Issue"
    assert new_issue.state == "OPEN"

    # Add comments
    issues.add_comment(sentinel_path(), 100, "Comment on existing")
    issues.add_comment(sentinel_path(), 200, "Comment on new")

    # List all issues
    all_issues = issues.list_issues(repo_root=sentinel_path())
    assert len(all_issues) == 2

    # Verify mutation tracking
    assert issues.created_issues == [("New Issue", "New body", ["plan", "erk"])]
    # Note: comment IDs start at 1000
    assert issues.added_comments == [
        (100, "Comment on existing", 1000),
        (200, "Comment on new", 1001),
    ]


def test_fake_github_issues_empty_labels() -> None:
    """Test create_issue with empty labels list."""
    issues = FakeGitHubIssues()

    result = issues.create_issue(repo_root=sentinel_path(), title="Title", body="Body", labels=[])

    assert result.number == 1
    assert issues.created_issues == [("Title", "Body", [])]


def test_fake_github_issues_label_filtering_implemented() -> None:
    """Test that label filtering filters issues by required labels."""
    pre_configured = {
        1: create_test_issue(
            1, "Issue 1", "Body 1", url="http://url/1", labels=["erk-plan", "bug"]
        ),
        2: create_test_issue(2, "Issue 2", "Body 2", url="http://url/2", labels=["erk-plan"]),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    # Filter for issues with a label that exists on issue 1 but not issue 2
    result = issues.list_issues(repo_root=sentinel_path(), labels=["erk-plan", "bug"])

    # Only issue 1 has both labels
    assert len(result) == 1
    assert result[0].number == 1


def test_fake_github_issues_label_filtering_returns_none_when_no_match() -> None:
    """Test that label filtering returns empty list when no issues match."""
    pre_configured = {
        1: create_test_issue(1, "Issue 1", "Body 1", url="http://url/1"),
        2: create_test_issue(2, "Issue 2", "Body 2", url="http://url/2"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    # Filter for a label that doesn't exist on any issue
    result = issues.list_issues(repo_root=sentinel_path(), labels=["nonexistent"])

    # No issues have this label
    assert len(result) == 0


def test_fake_github_issues_state_case_sensitivity() -> None:
    """Test state filtering handles uppercase/lowercase properly."""
    pre_configured = {
        1: create_test_issue(1, "Open Issue", "Body", url="http://url/1"),
        2: create_test_issue(2, "Closed Issue", "Body", "CLOSED", "http://url/2"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    # Lowercase "open" should match uppercase "OPEN" state
    result = issues.list_issues(repo_root=sentinel_path(), state="open")

    assert len(result) == 1
    assert result[0].state == "OPEN"


def test_fake_github_issues_mutation_tracking_independent() -> None:
    """Test that created_issues and added_comments track independently."""
    issues = FakeGitHubIssues(next_issue_number=1)

    # Create issues
    result1 = issues.create_issue(
        repo_root=sentinel_path(), title="Issue 1", body="Body 1", labels=["label1"]
    )
    result2 = issues.create_issue(
        repo_root=sentinel_path(), title="Issue 2", body="Body 2", labels=["label2"]
    )

    # Add comments
    comment_id1 = issues.add_comment(sentinel_path(), result1.number, "Comment 1")
    comment_id2 = issues.add_comment(sentinel_path(), result2.number, "Comment 2")

    # Verify both tracking lists are independent
    assert len(issues.created_issues) == 2
    assert len(issues.added_comments) == 2

    # Verify correct values
    assert issues.created_issues[0][0] == "Issue 1"
    assert issues.created_issues[1][0] == "Issue 2"
    assert issues.added_comments[0] == (1, "Comment 1", comment_id1)
    assert issues.added_comments[1] == (2, "Comment 2", comment_id2)


def test_fake_github_issues_pre_configured_and_created_coexist() -> None:
    """Test that pre-configured and dynamically created issues coexist."""
    pre_configured = {100: create_test_issue(100, "Pre-configured", "Body", url="http://url/100")}
    issues = FakeGitHubIssues(issues=pre_configured, next_issue_number=1)

    # Create new issue
    new_result = issues.create_issue(
        repo_root=sentinel_path(), title="New", body="Body", labels=["label"]
    )

    # Both should be retrievable
    pre_issue = issues.get_issue(sentinel_path(), 100)
    assert pre_issue.title == "Pre-configured"

    new_issue = issues.get_issue(sentinel_path(), new_result.number)
    assert new_issue.title == "New"

    # List should include both
    all_issues = issues.list_issues(repo_root=sentinel_path())
    assert len(all_issues) == 2


def test_fake_github_issues_url_generation() -> None:
    """Test that created issues get properly formatted URLs."""
    issues = FakeGitHubIssues(next_issue_number=42)

    result = issues.create_issue(repo_root=sentinel_path(), title="Title", body="Body", labels=[])

    created_issue = issues.get_issue(sentinel_path(), result.number)

    assert created_issue.url == "https://github.com/test-owner/test-repo/issues/42"


def test_fake_github_issues_created_state_always_open() -> None:
    """Test that created issues always have OPEN state."""
    issues = FakeGitHubIssues()

    result = issues.create_issue(repo_root=sentinel_path(), title="Title", body="Body", labels=[])

    created_issue = issues.get_issue(sentinel_path(), result.number)

    assert created_issue.state == "OPEN"


def test_fake_github_issues_multiple_comments_same_issue() -> None:
    """Test adding multiple comments to the same issue returns incrementing IDs."""
    pre_configured = {42: create_test_issue(42, "Test", "Body", url="http://url/42")}
    issues = FakeGitHubIssues(issues=pre_configured)

    id1 = issues.add_comment(sentinel_path(), 42, "Comment 1")
    id2 = issues.add_comment(sentinel_path(), 42, "Comment 2")
    id3 = issues.add_comment(sentinel_path(), 42, "Comment 3")

    # All comments should be tracked with incrementing IDs
    assert id1 == 1000
    assert id2 == 1001
    assert id3 == 1002
    assert issues.added_comments == [
        (42, "Comment 1", 1000),
        (42, "Comment 2", 1001),
        (42, "Comment 3", 1002),
    ]


def test_fake_github_issues_get_comment_by_id_from_added_comments() -> None:
    """Test get_comment_by_id retrieves comments added via add_comment."""
    pre_configured = {42: create_test_issue(42, "Test", "Body", url="http://url/42")}
    issues = FakeGitHubIssues(issues=pre_configured)

    # Add some comments
    comment_id = issues.add_comment(sentinel_path(), 42, "Comment body here")

    # Retrieve by ID
    body = issues.get_comment_by_id(sentinel_path(), comment_id)
    assert body == "Comment body here"


def test_fake_github_issues_get_comment_by_id_from_preconfigured() -> None:
    """Test get_comment_by_id retrieves comments from pre-configured _comments_with_urls."""
    from erk_shared.gateway.github.issues.types import IssueComment

    pre_configured = {42: create_test_issue(42, "Test", "Body", url="http://url/42")}
    comments_with_urls = {
        42: [IssueComment(body="Pre-configured comment", url="http://url", id=999, author="user")]
    }
    issues = FakeGitHubIssues(issues=pre_configured, comments_with_urls=comments_with_urls)

    # Retrieve pre-configured comment by ID
    body = issues.get_comment_by_id(sentinel_path(), 999)
    assert body == "Pre-configured comment"


def test_fake_github_issues_get_comment_by_id_not_found() -> None:
    """Test get_comment_by_id raises RuntimeError for unknown ID."""
    pre_configured = {42: create_test_issue(42, "Test", "Body", url="http://url/42")}
    issues = FakeGitHubIssues(issues=pre_configured)

    with pytest.raises(RuntimeError, match="Comment #99999 not found"):
        issues.get_comment_by_id(sentinel_path(), 99999)


def test_fake_github_issues_ensure_label_exists_creates_new() -> None:
    """Test ensure_label_exists creates label when it doesn't exist."""
    issues = FakeGitHubIssues()

    issues.ensure_label_exists(
        repo_root=sentinel_path(),
        label="erk-plan",
        description="Implementation plan created by erk",
        color="0E8A16",
    )

    assert "erk-plan" in issues.labels
    assert issues.created_labels == [("erk-plan", "Implementation plan created by erk", "0E8A16")]


def test_fake_github_issues_ensure_label_exists_idempotent() -> None:
    """Test ensure_label_exists doesn't create duplicate labels."""
    issues = FakeGitHubIssues(labels={"erk-plan"})

    issues.ensure_label_exists(
        repo_root=sentinel_path(),
        label="erk-plan",
        description="Implementation plan created by erk",
        color="0E8A16",
    )

    # Label already exists, no new creation
    assert "erk-plan" in issues.labels
    assert issues.created_labels == []


def test_fake_github_issues_ensure_label_exists_multiple() -> None:
    """Test ensure_label_exists tracks multiple label creations."""
    issues = FakeGitHubIssues()

    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="label1", description="Description 1", color="FF0000"
    )
    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="label2", description="Description 2", color="00FF00"
    )
    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="label3", description="Description 3", color="0000FF"
    )

    assert "label1" in issues.labels
    assert "label2" in issues.labels
    assert "label3" in issues.labels
    assert issues.created_labels == [
        ("label1", "Description 1", "FF0000"),
        ("label2", "Description 2", "00FF00"),
        ("label3", "Description 3", "0000FF"),
    ]


def test_fake_github_issues_ensure_label_exists_mixed_existing_new() -> None:
    """Test ensure_label_exists with mix of existing and new labels."""
    issues = FakeGitHubIssues(labels={"existing-label"})

    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="existing-label", description="Desc 1", color="111111"
    )
    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="new-label", description="Desc 2", color="222222"
    )

    # Only new label should be in created_labels
    assert "existing-label" in issues.labels
    assert "new-label" in issues.labels
    assert issues.created_labels == [("new-label", "Desc 2", "222222")]


def test_fake_github_issues_labels_property_read_only() -> None:
    """Test labels property returns a copy (read-only access)."""
    issues = FakeGitHubIssues(labels={"label1"})

    labels = issues.labels
    labels.add("label2")  # Modify the returned copy

    # Original should be unchanged
    assert "label2" not in issues.labels
    assert issues.labels == {"label1"}


def test_fake_github_issues_created_labels_empty_initially() -> None:
    """Test created_labels property is empty list initially."""
    issues = FakeGitHubIssues()

    assert issues.created_labels == []


def test_fake_github_issues_created_labels_read_only() -> None:
    """Test created_labels property returns list that can be read."""
    issues = FakeGitHubIssues()
    issues.ensure_label_exists(
        repo_root=sentinel_path(),
        label="test-label",
        description="Test description",
        color="000000",
    )

    # Should be able to read the list
    created = issues.created_labels
    assert len(created) == 1
    assert created[0] == ("test-label", "Test description", "000000")


def test_list_issues_respects_limit() -> None:
    """Test list_issues applies limit correctly."""
    now = datetime.now(UTC)
    issues_dict = {
        1: create_test_issue(
            number=1,
            title="Issue 1",
            body="Body 1",
            created_at=now,
            updated_at=now,
        ),
        2: create_test_issue(
            number=2,
            title="Issue 2",
            body="Body 2",
            created_at=now,
            updated_at=now,
        ),
        3: create_test_issue(
            number=3,
            title="Issue 3",
            body="Body 3",
            created_at=now,
            updated_at=now,
        ),
    }
    fake = FakeGitHubIssues(issues=issues_dict)

    # Test limit=2 returns only 2 issues
    result = fake.list_issues(repo_root=sentinel_path(), limit=2)
    assert len(result) == 2

    # Test limit=None returns all issues
    result = fake.list_issues(repo_root=sentinel_path(), limit=None)
    assert len(result) == 3


def test_get_issue_comments_empty() -> None:
    """Test get_issue_comments returns empty list when no comments exist."""
    fake = FakeGitHubIssues()

    result = fake.get_issue_comments(sentinel_path(), 123)

    assert result == []


def test_get_issue_comments_with_comments() -> None:
    """Test get_issue_comments returns comments for issue."""
    comments = {
        42: ["First comment", "Second comment"],
        100: ["Another comment"],
    }
    fake = FakeGitHubIssues(comments=comments)

    result = fake.get_issue_comments(sentinel_path(), 42)

    assert result == ["First comment", "Second comment"]


def test_get_current_username_default() -> None:
    """Test get_current_username returns default 'testuser'."""
    fake = FakeGitHubIssues()

    result = fake.get_current_username()

    assert result == "testuser"


def test_get_current_username_custom() -> None:
    """Test get_current_username returns custom username from constructor."""
    fake = FakeGitHubIssues(username="custom-user")

    result = fake.get_current_username()

    assert result == "custom-user"


def test_get_current_username_none() -> None:
    """Test get_current_username returns None when simulating unauthenticated state."""
    fake = FakeGitHubIssues(username=None)

    result = fake.get_current_username()

    assert result is None


# ============================================================================
# close_issue() tests
# ============================================================================


def test_close_issue_updates_state() -> None:
    """Test close_issue changes issue state from OPEN to closed."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body")}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.close_issue(sentinel_path(), 42)

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.state == "closed"


def test_close_issue_missing_raises() -> None:
    """Test close_issue raises RuntimeError for non-existent issue."""
    issues = FakeGitHubIssues()

    with pytest.raises(RuntimeError, match="Issue #999 not found"):
        issues.close_issue(sentinel_path(), 999)


def test_close_issue_tracks_mutation() -> None:
    """Test close_issue tracks closed issue number in closed_issues list."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body")}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.close_issue(sentinel_path(), 42)

    assert 42 in issues.closed_issues


def test_close_issue_preserves_other_fields() -> None:
    """Test close_issue preserves title, body, labels unchanged."""
    pre_configured = {
        42: create_test_issue(42, "Test Issue", "Test body", labels=["bug", "urgent"])
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.close_issue(sentinel_path(), 42)

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.title == "Test Issue"
    assert updated_issue.body == "Test body"
    assert updated_issue.labels == ["bug", "urgent"]
    assert updated_issue.number == 42


# ============================================================================
# closed_issues property tests
# ============================================================================


def test_closed_issues_empty_initially() -> None:
    """Test closed_issues property is empty list on new instance."""
    issues = FakeGitHubIssues()

    assert issues.closed_issues == []


def test_closed_issues_tracks_multiple() -> None:
    """Test closed_issues tracks multiple closes in order."""
    pre_configured = {
        10: create_test_issue(10, "Issue 10", "Body"),
        20: create_test_issue(20, "Issue 20", "Body"),
        30: create_test_issue(30, "Issue 30", "Body"),
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.close_issue(sentinel_path(), 20)
    issues.close_issue(sentinel_path(), 10)
    issues.close_issue(sentinel_path(), 30)

    assert issues.closed_issues == [20, 10, 30]


# ============================================================================
# ensure_label_on_issue() tests
# ============================================================================


def test_ensure_label_on_issue_adds_label() -> None:
    """Test ensure_label_on_issue adds label when not present."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body", labels=[])}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.ensure_label_on_issue(sentinel_path(), 42, "new-label")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert "new-label" in updated_issue.labels


def test_ensure_label_on_issue_idempotent() -> None:
    """Test ensure_label_on_issue is idempotent (no duplicate when already present)."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body", labels=["existing"])}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.ensure_label_on_issue(sentinel_path(), 42, "existing")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    # Label should appear only once
    assert updated_issue.labels.count("existing") == 1


def test_ensure_label_on_issue_missing_raises() -> None:
    """Test ensure_label_on_issue raises RuntimeError for non-existent issue."""
    issues = FakeGitHubIssues()

    with pytest.raises(RuntimeError, match="Issue #999 not found"):
        issues.ensure_label_on_issue(sentinel_path(), 999, "label")


def test_ensure_label_on_issue_appends_to_existing() -> None:
    """Test ensure_label_on_issue preserves existing labels."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body", labels=["label1", "label2"])}
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.ensure_label_on_issue(sentinel_path(), 42, "label3")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.labels == ["label1", "label2", "label3"]


# ============================================================================
# remove_label_from_issue() tests
# ============================================================================


def test_remove_label_from_issue_removes() -> None:
    """Test remove_label_from_issue removes label when present."""
    pre_configured = {
        42: create_test_issue(42, "Test Issue", "Body", labels=["bug", "enhancement"])
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.remove_label_from_issue(sentinel_path(), 42, "bug")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert "bug" not in updated_issue.labels
    assert "enhancement" in updated_issue.labels


def test_remove_label_from_issue_missing_raises() -> None:
    """Test remove_label_from_issue raises RuntimeError for non-existent issue."""
    issues = FakeGitHubIssues()

    with pytest.raises(RuntimeError, match="Issue #999 not found"):
        issues.remove_label_from_issue(sentinel_path(), 999, "label")


def test_remove_label_from_issue_preserves_others() -> None:
    """Test remove_label_from_issue preserves other labels unchanged."""
    pre_configured = {
        42: create_test_issue(42, "Test Issue", "Body", labels=["keep1", "remove", "keep2"])
    }
    issues = FakeGitHubIssues(issues=pre_configured)

    issues.remove_label_from_issue(sentinel_path(), 42, "remove")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.labels == ["keep1", "keep2"]


def test_remove_label_from_issue_idempotent() -> None:
    """Test remove_label_from_issue is idempotent (no error when label not on issue)."""
    pre_configured = {42: create_test_issue(42, "Test Issue", "Body", labels=["other"])}
    issues = FakeGitHubIssues(issues=pre_configured)

    # Should not raise error when label doesn't exist on issue
    issues.remove_label_from_issue(sentinel_path(), 42, "nonexistent")

    updated_issue = issues.get_issue(sentinel_path(), 42)
    assert updated_issue.labels == ["other"]


# ============================================================================
# get_prs_referencing_issue() tests
# ============================================================================


def test_get_prs_referencing_issue_empty() -> None:
    """Test get_prs_referencing_issue returns empty list when no PRs configured."""
    issues = FakeGitHubIssues()

    result = issues.get_prs_referencing_issue(sentinel_path(), 42)

    assert result == []


def test_get_prs_referencing_issue_returns_configured_prs() -> None:
    """Test get_prs_referencing_issue returns pre-configured PRs."""
    from erk_shared.gateway.github.issues.types import PRReference

    pr_refs = {
        42: [
            PRReference(number=100, state="OPEN", is_draft=True),
            PRReference(number=101, state="MERGED", is_draft=False),
        ],
        99: [
            PRReference(number=200, state="CLOSED", is_draft=False),
        ],
    }
    issues = FakeGitHubIssues(pr_references=pr_refs)

    result = issues.get_prs_referencing_issue(sentinel_path(), 42)

    assert len(result) == 2
    assert result[0].number == 100
    assert result[0].state == "OPEN"
    assert result[0].is_draft is True
    assert result[1].number == 101
    assert result[1].state == "MERGED"
    assert result[1].is_draft is False


def test_get_prs_referencing_issue_different_issue_numbers() -> None:
    """Test get_prs_referencing_issue returns correct PRs for each issue."""
    from erk_shared.gateway.github.issues.types import PRReference

    pr_refs = {
        10: [PRReference(number=100, state="OPEN", is_draft=True)],
        20: [PRReference(number=200, state="CLOSED", is_draft=False)],
    }
    issues = FakeGitHubIssues(pr_references=pr_refs)

    result_10 = issues.get_prs_referencing_issue(sentinel_path(), 10)
    result_20 = issues.get_prs_referencing_issue(sentinel_path(), 20)
    result_30 = issues.get_prs_referencing_issue(sentinel_path(), 30)

    assert len(result_10) == 1
    assert result_10[0].number == 100
    assert len(result_20) == 1
    assert result_20[0].number == 200
    assert result_30 == []  # No PRs configured for issue 30


# ============================================================================
# label_exists() tests
# ============================================================================


def test_label_exists_returns_true_for_existing_label() -> None:
    """Test label_exists returns True when label exists in fake storage."""
    issues = FakeGitHubIssues(labels={"erk-plan", "erk-objective"})

    assert issues.label_exists(sentinel_path(), "erk-plan") is True
    assert issues.label_exists(sentinel_path(), "erk-objective") is True


def test_label_exists_returns_false_for_missing_label() -> None:
    """Test label_exists returns False when label doesn't exist."""
    issues = FakeGitHubIssues(labels={"erk-plan"})

    assert issues.label_exists(sentinel_path(), "nonexistent") is False


def test_label_exists_empty_labels() -> None:
    """Test label_exists returns False when no labels configured."""
    issues = FakeGitHubIssues()

    assert issues.label_exists(sentinel_path(), "any-label") is False


def test_label_exists_after_ensure_label_exists() -> None:
    """Test label_exists returns True after ensure_label_exists creates it."""
    issues = FakeGitHubIssues()

    # Initially doesn't exist
    assert issues.label_exists(sentinel_path(), "new-label") is False

    # Create it
    issues.ensure_label_exists(
        repo_root=sentinel_path(), label="new-label", description="Description", color="FF0000"
    )

    # Now it exists
    assert issues.label_exists(sentinel_path(), "new-label") is True
