"""Tests for the circuit breaker mechanism that detects systemic failures
when pulling commits or PRs/MRs across repos."""

import pytest

from jf_ingest.config import GitAuthConfig, GitConfig, GitProvider
from jf_ingest.jf_git.adapters import GitAdapter
from jf_ingest.jf_git.adapters.github import GithubAdapter
from jf_ingest.jf_git.exceptions import GitCircuitBreakerException
from jf_ingest.jf_git.standardized_models import (
    StandardizedOrganization,
    StandardizedRepository,
)


# ---------------------------------------------------------------------------
# Test fixtures and helpers
# ---------------------------------------------------------------------------

TEST_GIT_AUTH_CONFIG = GitAuthConfig(
    company_slug='TEST COMPANY',
    base_url='www.base.com',
    token='apples',
    verify=False,
)

BASE_ORGANIZATION = StandardizedOrganization(
    id='org_login',
    name='An Organization',
    login='org_login',
    url='www.organization.com',
)

GITHUB_CONFIG = GitConfig(
    company_slug=TEST_GIT_AUTH_CONFIG.company_slug,
    instance_slug='test_instance_slug',
    instance_file_key='TEST_INSTANCE_FILE_KEY',
    git_provider=GitProvider.GITHUB,
    git_auth_config=TEST_GIT_AUTH_CONFIG,
    git_organizations=[BASE_ORGANIZATION.login],
)


def _make_repo(repo_id: str) -> StandardizedRepository:
    """Create a minimal StandardizedRepository for testing."""
    return StandardizedRepository(
        id=repo_id,
        name=f'repo_{repo_id}',
        full_name=f'org/repo_{repo_id}',
        url=f'www.repo-{repo_id}.com',
        is_fork=False,
        default_branch_name='main',
        default_branch_sha='abc123',
        organization=BASE_ORGANIZATION,
    )


def _make_repos(count: int) -> list[StandardizedRepository]:
    """Create a list of StandardizedRepository objects for testing."""
    return [_make_repo(str(i)) for i in range(count)]


@pytest.fixture
def adapter() -> GitAdapter:
    """Create a GithubAdapter instance (concrete subclass of GitAdapter)."""
    a = GithubAdapter(GITHUB_CONFIG)
    a._init_circuit_breaker_trackers()
    return a


# ---------------------------------------------------------------------------
# GitCircuitBreakerException tests
# ---------------------------------------------------------------------------


class TestGitCircuitBreakerException:
    def test_exception_attributes(self):
        exc = GitCircuitBreakerException(
            commit_failure_count=9,
            commit_total_count=10,
            commit_failure_rate=0.9,
            pr_failure_count=8,
            pr_total_count=10,
            pr_failure_rate=0.8,
        )
        assert exc.commit_failure_count == 9
        assert exc.commit_total_count == 10
        assert exc.commit_failure_rate == 0.9
        assert exc.pr_failure_count == 8
        assert exc.pr_total_count == 10
        assert exc.pr_failure_rate == 0.8

    def test_exception_message_both_tripped(self):
        exc = GitCircuitBreakerException(
            commit_failure_count=5,
            commit_total_count=5,
            commit_failure_rate=1.0,
            pr_failure_count=5,
            pr_total_count=5,
            pr_failure_rate=1.0,
        )
        msg = str(exc)
        assert 'commits:' in msg
        assert 'PRs/MRs:' in msg
        assert '5/5' in msg

    def test_exception_message_only_commits_tripped(self):
        exc = GitCircuitBreakerException(
            commit_failure_count=5,
            commit_total_count=5,
            commit_failure_rate=1.0,
            pr_failure_count=0,
            pr_total_count=5,
            pr_failure_rate=0.0,
        )
        msg = str(exc)
        assert 'commits:' in msg
        assert 'PRs/MRs:' not in msg

    def test_exception_message_only_prs_tripped(self):
        exc = GitCircuitBreakerException(
            commit_failure_count=0,
            commit_total_count=5,
            commit_failure_rate=0.0,
            pr_failure_count=5,
            pr_total_count=5,
            pr_failure_rate=1.0,
        )
        msg = str(exc)
        assert 'commits:' not in msg
        assert 'PRs/MRs:' in msg

    def test_threshold_constant(self):
        assert GitCircuitBreakerException.FAILURE_THRESHOLD == 0.80

    def test_is_exception_subclass(self):
        exc = GitCircuitBreakerException(
            commit_failure_count=1, commit_total_count=1, commit_failure_rate=1.0,
            pr_failure_count=1, pr_total_count=1, pr_failure_rate=1.0,
        )
        assert isinstance(exc, Exception)


# ---------------------------------------------------------------------------
# _record_repo_data_attempt tests
# ---------------------------------------------------------------------------


class TestRecordRepoDataAttempt:
    def test_marks_commit_attempt(self, adapter: GitAdapter):
        adapter._record_repo_data_attempt('repo_1', 'commits')
        assert adapter._commit_success_tracker['repo_1'] is True

    def test_marks_pr_attempt(self, adapter: GitAdapter):
        adapter._record_repo_data_attempt('repo_1', 'prs')
        assert adapter._pr_success_tracker['repo_1'] is True

    def test_does_not_override_failure(self, adapter: GitAdapter):
        """If a repo is already marked as failed, a subsequent attempt call
        should NOT override it back to True (setdefault behavior)."""
        adapter._commit_success_tracker['repo_1'] = False
        adapter._record_repo_data_attempt('repo_1', 'commits')
        assert adapter._commit_success_tracker['repo_1'] is False

    def test_does_not_override_existing_success(self, adapter: GitAdapter):
        """Calling attempt multiple times should be idempotent."""
        adapter._record_repo_data_attempt('repo_1', 'commits')
        adapter._record_repo_data_attempt('repo_1', 'commits')
        assert adapter._commit_success_tracker['repo_1'] is True

    def test_ignores_invalid_data_type(self, adapter: GitAdapter):
        adapter._record_repo_data_attempt('repo_1', 'invalid')
        assert 'repo_1' not in adapter._commit_success_tracker
        assert 'repo_1' not in adapter._pr_success_tracker


# ---------------------------------------------------------------------------
# _record_repo_data_error tests
# ---------------------------------------------------------------------------


class TestRecordRepoDataError:
    def test_record_commit_error(self, adapter: GitAdapter):
        adapter._commit_success_tracker['repo_1'] = True
        adapter._record_repo_data_error('repo_1', 'commits')
        assert adapter._commit_success_tracker['repo_1'] is False

    def test_record_pr_error(self, adapter: GitAdapter):
        adapter._pr_success_tracker['repo_1'] = True
        adapter._record_repo_data_error('repo_1', 'prs')
        assert adapter._pr_success_tracker['repo_1'] is False

    def test_record_error_for_new_repo(self, adapter: GitAdapter):
        """Recording an error for a repo not yet in the tracker should still work."""
        adapter._record_repo_data_error('unknown_repo', 'commits')
        assert adapter._commit_success_tracker['unknown_repo'] is False

    def test_ignores_invalid_data_type(self, adapter: GitAdapter):
        """Invalid data_type should not affect trackers."""
        adapter._record_repo_data_error('repo_1', 'invalid')
        assert 'repo_1' not in adapter._commit_success_tracker
        assert 'repo_1' not in adapter._pr_success_tracker


# ---------------------------------------------------------------------------
# _check_circuit_breaker tests
# ---------------------------------------------------------------------------


class TestCheckCircuitBreaker:
    def test_all_repos_fail_commits_and_prs(self, adapter: GitAdapter):
        """When all repos fail both commits and PRs, the circuit breaker should trigger."""
        repos = _make_repos(5)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = False
            adapter._pr_success_tracker[repo.id] = False

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 5
        assert result.commit_total_count == 5
        assert result.commit_failure_rate == 1.0
        assert result.pr_failure_count == 5
        assert result.pr_total_count == 5
        assert result.pr_failure_rate == 1.0

    def test_all_repos_fail_commits_only(self, adapter: GitAdapter):
        """When >80% of repos fail commits but PRs succeed, the circuit breaker
        should still trigger based on commits alone."""
        repos = _make_repos(5)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = False
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_rate == 1.0
        assert result.pr_failure_rate == 0.0

    def test_all_repos_fail_prs_only(self, adapter: GitAdapter):
        """When >80% of repos fail PRs but commits succeed, the circuit breaker
        should still trigger based on PRs alone."""
        repos = _make_repos(5)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = True
            adapter._pr_success_tracker[repo.id] = False

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_rate == 0.0
        assert result.pr_failure_rate == 1.0

    def test_no_repos_fail(self, adapter: GitAdapter):
        """When all repos succeed, the circuit breaker should NOT trigger."""
        repos = _make_repos(5)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = True
            adapter._pr_success_tracker[repo.id] = True

        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_exactly_80_percent_commits_does_not_trigger(self, adapter: GitAdapter):
        """Exactly 80% commit failure rate should NOT trigger (threshold is >80%)."""
        repos = _make_repos(5)
        # 4 out of 5 fail commits = 80%
        for repo in repos[:4]:
            adapter._commit_success_tracker[repo.id] = False
        adapter._commit_success_tracker[repos[4].id] = True
        for repo in repos:
            adapter._pr_success_tracker[repo.id] = True

        # 80% is not > 80%
        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_exactly_80_percent_prs_does_not_trigger(self, adapter: GitAdapter):
        """Exactly 80% PR failure rate should NOT trigger (threshold is >80%)."""
        repos = _make_repos(5)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = True
        for repo in repos[:4]:
            adapter._pr_success_tracker[repo.id] = False
        adapter._pr_success_tracker[repos[4].id] = True

        # 80% is not > 80%
        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_just_above_80_percent_commits_triggers(self, adapter: GitAdapter):
        """Just above 80% commit failure rate should trigger, even if PRs are fine."""
        repos = _make_repos(6)
        # 5 out of 6 fail commits = 83.3%
        for repo in repos[:5]:
            adapter._commit_success_tracker[repo.id] = False
        adapter._commit_success_tracker[repos[5].id] = True
        for repo in repos:
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 5
        assert result.commit_total_count == 6
        assert result.pr_failure_rate == 0.0

    def test_just_above_80_percent_prs_triggers(self, adapter: GitAdapter):
        """Just above 80% PR failure rate should trigger, even if commits are fine."""
        repos = _make_repos(6)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = True
        for repo in repos[:5]:
            adapter._pr_success_tracker[repo.id] = False
        adapter._pr_success_tracker[repos[5].id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.pr_failure_count == 5
        assert result.pr_total_count == 6
        assert result.commit_failure_rate == 0.0

    def test_below_80_percent_does_not_trigger(self, adapter: GitAdapter):
        """Below 80% failure rate for both commits and PRs should NOT trigger."""
        repos = _make_repos(10)
        # 7 out of 10 fail both = 70%
        for repo in repos[:7]:
            adapter._commit_success_tracker[repo.id] = False
            adapter._pr_success_tracker[repo.id] = False
        for repo in repos[7:]:
            adapter._commit_success_tracker[repo.id] = True
            adapter._pr_success_tracker[repo.id] = True

        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_zero_repos(self, adapter: GitAdapter):
        """Zero repos should NOT trigger the circuit breaker."""
        assert adapter._check_circuit_breaker([], []) is None

    def test_single_repo_success(self, adapter: GitAdapter):
        """A single repo that succeeds should not trigger."""
        repos = _make_repos(1)
        adapter._commit_success_tracker[repos[0].id] = True
        adapter._pr_success_tracker[repos[0].id] = True

        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_single_repo_commit_failure(self, adapter: GitAdapter):
        """A single repo that fails commits should trigger (100% > 80%)."""
        repos = _make_repos(1)
        adapter._commit_success_tracker[repos[0].id] = False
        adapter._pr_success_tracker[repos[0].id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 1
        assert result.commit_total_count == 1
        assert result.commit_failure_rate == 1.0
        assert result.pr_failure_rate == 0.0

    def test_single_repo_pr_failure(self, adapter: GitAdapter):
        """A single repo that fails PRs should trigger (100% > 80%)."""
        repos = _make_repos(1)
        adapter._commit_success_tracker[repos[0].id] = True
        adapter._pr_success_tracker[repos[0].id] = False

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.pr_failure_count == 1
        assert result.pr_total_count == 1
        assert result.pr_failure_rate == 1.0
        assert result.commit_failure_rate == 0.0

    def test_mixed_commit_failures_above_pr_failures_below(self, adapter: GitAdapter):
        """Commits >80% failure, PRs <80% failure -> triggers because of commits."""
        repos = _make_repos(10)
        # 9/10 repos fail commits = 90%
        for repo in repos[:9]:
            adapter._commit_success_tracker[repo.id] = False
        adapter._commit_success_tracker[repos[9].id] = True
        # 5/10 repos fail PRs = 50%
        for repo in repos[:5]:
            adapter._pr_success_tracker[repo.id] = False
        for repo in repos[5:]:
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 9
        assert result.commit_failure_rate == 0.9
        # PR rate is included in the exception but did not trip the breaker
        assert result.pr_failure_count == 5
        assert result.pr_failure_rate == 0.5

    def test_mixed_pr_failures_above_commit_failures_below(self, adapter: GitAdapter):
        """PRs >80% failure, commits <80% failure -> triggers because of PRs."""
        repos = _make_repos(10)
        # 5/10 repos fail commits = 50%
        for repo in repos[:5]:
            adapter._commit_success_tracker[repo.id] = False
        for repo in repos[5:]:
            adapter._commit_success_tracker[repo.id] = True
        # 9/10 repos fail PRs = 90%
        for repo in repos[:9]:
            adapter._pr_success_tracker[repo.id] = False
        adapter._pr_success_tracker[repos[9].id] = True

        result = adapter._check_circuit_breaker(repos, repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.pr_failure_count == 9
        assert result.pr_failure_rate == 0.9
        assert result.commit_failure_count == 5
        assert result.commit_failure_rate == 0.5

    def test_both_commit_and_pr_failures_below_threshold(self, adapter: GitAdapter):
        """Both commits and PRs below 80% failure -> does NOT trigger."""
        repos = _make_repos(10)
        # 7/10 fail commits = 70%, 6/10 fail PRs = 60%
        for repo in repos[:7]:
            adapter._commit_success_tracker[repo.id] = False
        for repo in repos[7:]:
            adapter._commit_success_tracker[repo.id] = True
        for repo in repos[:6]:
            adapter._pr_success_tracker[repo.id] = False
        for repo in repos[6:]:
            adapter._pr_success_tracker[repo.id] = True

        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_different_commit_and_pr_repo_lists(self, adapter: GitAdapter):
        """repos_to_process and repos_to_process_prs_for can be different lists
        (some repos skip PRs due to avoid_pulling_prs config)."""
        all_repos = _make_repos(5)
        commit_repos = all_repos  # All 5 repos process commits
        pr_repos = all_repos[:3]  # Only 3 repos process PRs

        # All 5 repos succeed commits
        for repo in all_repos:
            adapter._commit_success_tracker[repo.id] = True

        # Only 3 repos in PR list - all succeed
        for repo in pr_repos:
            adapter._pr_success_tracker[repo.id] = True

        assert adapter._check_circuit_breaker(commit_repos, pr_repos) is None

    def test_different_repo_lists_commits_fail_prs_succeed(self, adapter: GitAdapter):
        """All commits fail (5/5=100%), but PRs succeed (0/3=0%). Should trigger
        because commits exceeded threshold."""
        all_repos = _make_repos(5)
        commit_repos = all_repos
        pr_repos = all_repos[:3]

        for repo in all_repos:
            adapter._commit_success_tracker[repo.id] = False
        for repo in pr_repos:
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(commit_repos, pr_repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 5
        assert result.commit_total_count == 5
        assert result.pr_failure_rate == 0.0
        assert result.pr_total_count == 3

    def test_valid_empty_responses_are_not_failures(self, adapter: GitAdapter):
        """Repos that yield zero data (valid empty response) should NOT be treated as failures.

        This is the key design requirement: the circuit breaker only counts actual errors
        (exceptions), not repos that legitimately have no data in the date range."""
        repos = _make_repos(5)

        # All repos attempted the client call and succeeded (yielded 0 items = valid empty)
        for repo in repos:
            adapter._commit_success_tracker[repo.id] = True
            adapter._pr_success_tracker[repo.id] = True

        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_skipped_repos_excluded_from_denominator(self, adapter: GitAdapter):
        """Repos that were short-circuited (e.g., by metadata cache indicating no new data)
        should NOT appear in the tracker and should NOT count toward the denominator.

        This prevents inflating the denominator and masking systemic failures. For example,
        if 30 repos are skipped and 23 are attempted but all fail, the failure rate should
        be 23/23 = 100%, not 23/53 = 43.4%."""
        all_repos = _make_repos(10)
        # Only 3 repos actually attempted the client call, and all 3 failed
        for repo in all_repos[:3]:
            adapter._commit_success_tracker[repo.id] = False
        # The other 7 repos were short-circuited and are NOT in the tracker

        # All repos attempted PRs and succeeded
        for repo in all_repos:
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(all_repos, all_repos)

        assert isinstance(result, GitCircuitBreakerException)
        # Denominator should be 3 (attempted), not 10 (total)
        assert result.commit_failure_count == 3
        assert result.commit_total_count == 3
        assert result.commit_failure_rate == 1.0
        assert result.pr_failure_rate == 0.0

    def test_skipped_repos_prevent_false_dilution(self, adapter: GitAdapter):
        """If most repos are skipped and the few that are attempted all fail,
        the circuit breaker should trigger -- not be diluted by the skipped repos."""
        all_repos = _make_repos(53)  # Mirrors real scenario from testing
        attempted_repos = all_repos[:23]

        # 23 repos attempted and all failed
        for repo in attempted_repos:
            adapter._commit_success_tracker[repo.id] = False

        # The other 30 are NOT in the commit tracker (short-circuited)

        # PRs: all attempted and succeeded
        for repo in all_repos:
            adapter._pr_success_tracker[repo.id] = True

        result = adapter._check_circuit_breaker(all_repos, all_repos)

        assert isinstance(result, GitCircuitBreakerException)
        assert result.commit_failure_count == 23
        assert result.commit_total_count == 23  # NOT 53
        assert result.commit_failure_rate == 1.0

    def test_no_repos_attempted_does_not_trigger(self, adapter: GitAdapter):
        """If all repos were short-circuited (none attempted), the circuit breaker
        should NOT trigger -- there is no evidence of failure."""
        repos = _make_repos(5)
        # No repos in the tracker -- all were short-circuited
        assert adapter._check_circuit_breaker(repos, repos) is None


# ---------------------------------------------------------------------------
# _init_circuit_breaker_trackers tests
# ---------------------------------------------------------------------------


class TestInitCircuitBreakerTrackers:
    def test_initializes_empty_trackers(self, adapter: GitAdapter):
        """Trackers should be empty after initialization."""
        # Dirty the trackers first
        adapter._commit_success_tracker['some_repo'] = True
        adapter._pr_success_tracker['some_repo'] = False

        adapter._init_circuit_breaker_trackers()

        assert adapter._commit_success_tracker == {}
        assert adapter._pr_success_tracker == {}


# ---------------------------------------------------------------------------
# Integration-style tests: silent skip behavior preserved
# ---------------------------------------------------------------------------


class TestSilentSkipBehavior:
    """Verify that the existing silent skip behavior still works correctly
    when the failure rate is below the circuit breaker threshold."""

    def test_minority_failures_do_not_trigger(self, adapter: GitAdapter):
        """When a minority of repos fail (e.g., 1 out of 10), the circuit breaker
        should not trigger. This simulates the normal case where a few repos have
        higher security settings."""
        repos = _make_repos(10)

        # 1 repo fails commits, rest succeed
        adapter._commit_success_tracker[repos[0].id] = False
        for repo in repos[1:]:
            adapter._commit_success_tracker[repo.id] = True

        # All repos succeed PRs
        for repo in repos:
            adapter._pr_success_tracker[repo.id] = True

        # 1/10 = 10% commit failure rate -> well below threshold
        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_half_repos_fail_does_not_trigger(self, adapter: GitAdapter):
        """Even 50% failure rate for both should not trigger the circuit breaker."""
        repos = _make_repos(10)

        # 5 repos fail, 5 succeed
        for repo in repos[:5]:
            adapter._commit_success_tracker[repo.id] = False
            adapter._pr_success_tracker[repo.id] = False
        for repo in repos[5:]:
            adapter._commit_success_tracker[repo.id] = True
            adapter._pr_success_tracker[repo.id] = True

        # 5/10 = 50% for both -> below threshold
        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_two_repos_one_fails(self, adapter: GitAdapter):
        """With 2 repos and 1 failure, that is 50% which is below threshold."""
        repos = _make_repos(2)
        adapter._commit_success_tracker[repos[0].id] = False
        adapter._pr_success_tracker[repos[0].id] = False
        adapter._commit_success_tracker[repos[1].id] = True
        adapter._pr_success_tracker[repos[1].id] = True

        # 1/2 = 50% -> below threshold
        assert adapter._check_circuit_breaker(repos, repos) is None

    def test_five_repos_four_fail_at_boundary(self, adapter: GitAdapter):
        """With 5 repos and 4 failures, that is exactly 80% which does NOT trigger."""
        repos = _make_repos(5)
        for repo in repos[:4]:
            adapter._commit_success_tracker[repo.id] = False
            adapter._pr_success_tracker[repo.id] = False
        adapter._commit_success_tracker[repos[4].id] = True
        adapter._pr_success_tracker[repos[4].id] = True

        # 4/5 = 80% for both -> at threshold, NOT above it
        assert adapter._check_circuit_breaker(repos, repos) is None
