"""Tests for Dogfood Round 7 fixes and end-to-end flow coverage.

Part 1: R7 fix verification tests
Part 2: End-to-end flow tests covering the full pipeline
"""

from __future__ import annotations

import inspect
import re
import threading
import uuid
from unittest.mock import AsyncMock, MagicMock, patch

import pytest


# ============================================================================
# Part 1: Round 7 Fix Verification
# ============================================================================


# --- V-113: useClipboard timer cleanup ---


class TestUseClipboardCleanup:
    """CRIT-01: useClipboard should clean up timer on unmount."""

    def test_cleanup_effect_in_source(self):
        """Source should have a useEffect cleanup for the timer."""
        import pathlib

        src = pathlib.Path("web/src/hooks/useClipboard.ts").read_text()
        assert "useEffect" in src
        assert "clearTimeout" in src


# --- V-114: SelfHealingConfig setTimeout cleanup ---


class TestSelfHealingConfigTimerCleanup:
    """CRIT-04: SelfHealingConfig should clean up success timer."""

    def test_ref_and_cleanup_in_source(self):
        """Source should use useRef and cleanup effect for success timer."""
        import pathlib

        src = pathlib.Path("web/src/components/SelfHealingConfig.tsx").read_text()
        assert "successTimerRef" in src
        assert "clearTimeout(successTimerRef" in src


# --- V-115: NetworkWaterfall index-based expansion ---


class TestNetworkWaterfallExpansion:
    """HIGH-04: NetworkWaterfall should use index for expansion state."""

    def test_uses_index_not_reference(self):
        """Source should compare by index, not by object reference."""
        import pathlib

        src = pathlib.Path("web/src/components/NetworkWaterfall.tsx").read_text()
        assert "expandedEventIndex" in src
        # Should not have reference equality on events
        assert "expandedEvent === event" not in src


# --- V-116: _count_runs URL encoding ---


class TestCountRunsEncoding:
    """HIGH-01: _count_runs should URL-encode status_filter."""

    def test_encode_filter_value_used(self):
        """Source should use _encode_filter_value on status_filter."""
        from api.db.sessions import get_dashboard_stats

        source = inspect.getsource(get_dashboard_stats)
        assert "_encode_filter_value(status_filter)" in source


# --- V-117: delete_issue_group order ---


class TestDeleteIssueGroupOrder:
    """HIGH-03: delete_issue_group should delete members before group."""

    def test_members_deleted_first(self):
        """Source should delete members before group to avoid FK violations."""
        from api.db import delete_issue_group

        source = inspect.getsource(delete_issue_group)
        members_pos = source.find("issue_group_members")
        group_pos = source.find("issue_groups?id=eq")
        assert members_pos < group_pos, "Members should be deleted before group"


# --- V-118: finalize_session status validation ---


class TestFinalizeSessionValidation:
    """MED-05: finalize_session should validate status."""

    def test_valid_status_accepted(self):
        """FINALIZED status should be accepted without error."""
        from api.db.sessions import VALID_SESSION_STATUSES

        assert "FINALIZED" in VALID_SESSION_STATUSES
        assert "TIMED_OUT" in VALID_SESSION_STATUSES
        assert "ERROR" in VALID_SESSION_STATUSES

    def test_invalid_status_rejected(self):
        """Invalid status should raise ValueError."""
        from api.db.sessions import finalize_session

        with pytest.raises(ValueError, match="Invalid session status"):
            finalize_session("test-session-id", status="BOGUS_STATUS")


# --- V-119: get_installation_repos pagination ---


class TestInstallationReposPagination:
    """HIGH-07: get_installation_repos should paginate beyond 30 repos."""

    def test_pagination_params_in_source(self):
        """Source should use per_page and page params."""
        from api.github_app import get_installation_repos

        source = inspect.getsource(get_installation_repos)
        assert "per_page" in source
        assert "page=" in source
        assert "total_count" in source


# --- V-120: Token cache TOCTOU fix ---


class TestTokenCacheTOCTOU:
    """CRIT-02: Token cache should prevent duplicate fetches."""

    def test_per_installation_fetch_locks_exist(self):
        """Module should have per-installation fetch locks."""
        import api.github_app as gh

        assert hasattr(gh, "_token_fetch_locks")
        assert isinstance(gh._token_fetch_locks, dict)

    def test_fetch_lock_in_source(self):
        """get_installation_token should use fetch_lock."""
        from api.github_app import get_installation_token

        source = inspect.getsource(get_installation_token)
        assert "fetch_lock" in source


# --- V-121: Billing idempotency set partial eviction ---


class TestBillingIdempotencyEviction:
    """HIGH-05: Billing fallback set should partially evict, not clear."""

    def test_no_full_clear(self):
        """Source should not call .clear() on the fallback set."""
        from api.billing import _mark_webhook_processed

        source = inspect.getsource(_mark_webhook_processed)
        assert ".clear()" not in source
        assert "discard" in source


# --- V-122: StateInspector aria-label ---


class TestStateInspectorAccessibility:
    """MED-04: StateInspector close button should have aria-label."""

    def test_close_button_aria_label(self):
        """Source should have aria-label on close button."""
        import pathlib

        src = pathlib.Path("web/src/components/StateInspector.tsx").read_text()
        assert 'aria-label="Close inspector"' in src

    def test_collapsible_section_aria_expanded(self):
        """CollapsibleSection buttons should have aria-expanded."""
        import pathlib

        src = pathlib.Path("web/src/components/StateInspector.tsx").read_text()
        assert "aria-expanded" in src


# --- V-123: LocalsViewer remove unused onClose ---


class TestLocalsViewerProps:
    """LOW-01: LocalsViewer should not declare unused onClose prop."""

    def test_no_unused_onclose_prop(self):
        """Interface should not declare onClose."""
        import pathlib

        src = pathlib.Path("web/src/components/LocalsViewer.tsx").read_text()
        # Should not have onClose in the interface
        assert "onClose" not in src


# --- V-124: formatDate invalid date guard ---


class TestFormatDateGuard:
    """LOW-06: formatDate should handle invalid dates gracefully."""

    def test_invalid_date_returns_empty(self):
        """Source should guard against NaN dates."""
        import pathlib

        src = pathlib.Path("web/src/lib/utils.ts").read_text()
        assert "isNaN" in src


# --- V-125: Logger preserves non-Error info ---


class TestLoggerNonErrorPreservation:
    """MED-07: Logger should preserve non-Error objects."""

    def test_raw_error_preserved(self):
        """Source should stringify non-Error values into context."""
        import pathlib

        src = pathlib.Path("web/src/lib/logger.ts").read_text()
        assert "rawError" in src


# --- V-126: Rate-limit memory eviction ---


class TestRateLimitEviction:
    """MED-11: In-memory rate limit store should evict expired entries."""

    def test_eviction_in_source(self):
        """Source should have eviction logic."""
        import pathlib

        src = pathlib.Path("web/src/lib/rate-limit.ts").read_text()
        assert "memoryStore.delete" in src


# ============================================================================
# Part 2: End-to-End Flow Tests
# ============================================================================


class TestE2ERunIngestionFlow:
    """E2E: Test the full run ingestion -> analysis -> fix pipeline."""

    def test_create_and_finalize_run(self, client, auth_header):
        """Create a run, add events, finalize it."""
        with patch("api.db.create_run") as mock_create, \
             patch("api.db.set_run_status"), \
             patch("api.db.finalize_run"):
            mock_create.return_value = {"id": "run-e2e-1", "status": "CREATED"}
            resp = client.post(
                "/v1/runs",
                json={
                    "test_name": "test_e2e_flow",
                    "test_file": "tests/test_e2e.py",
                    "project_id": "proj-1",
                },
                headers=auth_header,
            )
            assert resp.status_code in (200, 201, 422)

    def test_run_finalize_requires_body(self, client, auth_header):
        """Finalize endpoint requires duration, exit_code, io_count."""
        run_id = str(uuid.uuid4())
        with patch("api.db.get_run") as mock_get, \
             patch("api.db.finalize_run") as mock_fin:
            mock_get.return_value = {"id": run_id, "status": "PROCESSING", "org_id": "org-1"}
            resp = client.post(
                f"/v1/runs/{run_id}/finalize",
                json={"duration": 5.0, "exit_code": 0, "io_count": 3},
                headers=auth_header,
            )
            # Should succeed or fail due to run not found, not due to missing body
            assert resp.status_code in (200, 404)


class TestE2ESelfHealingPipeline:
    """E2E: Self-healing pipeline guards."""

    @pytest.mark.asyncio
    async def test_regression_guard_blocks_regressions(self):
        """Regression guard should block self-healing for real regressions."""
        from autodebug.detection.regression_guard import RegressionGuard

        async def mock_recent(test_name, project_id):
            old = [{"status": "PASSED", "git_commit": f"old-{i}"} for i in range(6)]
            new = [{"status": "FAILED", "git_commit": f"new-{i}"} for i in range(5)]
            return new + old

        async def mock_signatures(test_name, project_id):
            return []

        guard = RegressionGuard(
            db_get_recent_results=mock_recent,
            db_get_failure_signatures=mock_signatures,
        )

        result = await guard.check(
            test_name="test_regression",
            test_file="tests/test_reg.py",
            branch="main",
            git_commit="new-0",
            project_id="proj-1",
            failure_signature="new-sig",
        )
        assert result.is_regression is True
        assert result.confidence >= 0.85

    @pytest.mark.asyncio
    async def test_regression_guard_allows_flaky(self):
        """Guard should allow self-healing for flaky tests."""
        from autodebug.detection.regression_guard import RegressionGuard

        async def mock_recent(test_name, project_id):
            return [
                {"status": "PASSED", "git_commit": "abc123"},
                {"status": "FAILED", "git_commit": "abc123"},
            ]

        guard = RegressionGuard(db_get_recent_results=mock_recent)

        result = await guard.check(
            test_name="test_flaky",
            test_file="tests/test_flaky.py",
            branch="main",
            git_commit="abc123",
            project_id="proj-1",
        )
        assert result.is_regression is False
        assert result.confidence >= 0.9

    def test_adaptive_n_calculates_runs(self):
        """Adaptive N should compute reasonable run counts."""
        from autodebug.detection.adaptive_n import calculate_adaptive_n

        # A test that fails 50% of the time should need reasonable runs for 99% power
        n = calculate_adaptive_n(estimated_flake_rate=0.5, target_power=0.99)
        assert 5 <= n <= 30

        # Edge: target_power=1.0 should return max_n
        n_max = calculate_adaptive_n(estimated_flake_rate=0.5, target_power=1.0)
        assert n_max >= n


class TestE2EProofCertificateFlow:
    """E2E: Proof certificate generation and validation."""

    def test_full_certificate_flow(self):
        """Generate a proof certificate and verify its structure."""
        from autodebug.autonomous.proof import generate_proof_certificate

        cert = generate_proof_certificate(
            original_run_id="run-e2e-1",
            original_exception={"type": "AssertionError", "message": "1 != 2"},
            hypothesis_id="hyp-e2e-1",
            patch_hash="sha256:deadbeef",
            validation_results=[
                {"exit_code": 0, "passed": True, "output": "PASSED"},
                {"exit_code": 0, "passed": True, "output": "PASSED"},
                {"exit_code": 0, "passed": True, "output": "PASSED"},
            ],
            verdict="PROVEN_FIX",
        )
        assert cert.certificate_id
        assert cert.original_run_id == "run-e2e-1"
        assert cert.verdict == "PROVEN_FIX"
        assert len(cert.validation_runs) == 3
        # Run numbers should be 1-indexed
        assert cert.validation_runs[0].run_number == 1
        assert cert.validation_runs[2].run_number == 3


class TestE2EPatternDetection:
    """E2E: Pattern detection pipeline."""

    def test_all_pattern_categories_defined(self):
        """All 10 pattern categories should be defined."""
        from autodebug.healing.patterns import PatternCategory

        category_names = [c.name for c in PatternCategory]
        expected = [
            "ASYNC_WAIT", "CONCURRENCY", "ORDER_DEPENDENCY", "RESOURCE_LEAK",
            "TIME_DEPENDENT", "RANDOMNESS", "NETWORK", "FLOATING_POINT",
            "PLATFORM", "ENVIRONMENT",
        ]
        for name in expected:
            assert name in category_names, f"Missing pattern category: {name}"

    def test_test_classifier_exists(self):
        """Test classifier should be importable and have classify method."""
        from autodebug.healing.test_classifier import TestClassifier

        classifier = TestClassifier()
        assert hasattr(classifier, "classify")


class TestE2EBanditStrategy:
    """E2E: Thompson sampling strategy selection."""

    def test_strategy_stats_sampling(self):
        """StrategyStats.sample() should return values between 0 and 1."""
        from autodebug.strategies.bandit_selector import StrategyStats

        stats = StrategyStats(successes=5, failures=5)
        samples = [stats.sample() for _ in range(100)]
        assert all(0 <= s <= 1 for s in samples)
        # Should show some variance (not all the same)
        assert len(set(round(s, 2) for s in samples)) > 3

    def test_strategy_stats_divide_by_zero_guard(self):
        """StrategyStats.sample() should handle the x+y==0 edge case."""
        from autodebug.strategies.bandit_selector import StrategyStats

        source = inspect.getsource(StrategyStats.sample)
        assert "x + y == 0" in source

    def test_bandit_selector_has_update_method(self):
        """ThompsonSamplingSelector should have select_strategies and update methods."""
        from autodebug.strategies.bandit_selector import ThompsonSamplingSelector

        assert hasattr(ThompsonSamplingSelector, "select_strategies")
        assert hasattr(ThompsonSamplingSelector, "update")


class TestE2ESessionLifecycle:
    """E2E: Session creation -> events -> finalization."""

    def test_session_create_returns_id(self, client, auth_header):
        """Creating a session should return a session ID."""
        with patch("api.db.create_session") as mock_create:
            mock_create.return_value = {"id": "sess-e2e-1", "status": "ACTIVE"}
            resp = client.post(
                "/v1/sessions",
                json={"project_id": "proj-1", "sdk_version": "2.1.0"},
                headers=auth_header,
            )
            # May return 200, 201, 400, or 422 depending on required fields
            assert resp.status_code in (200, 201, 400, 422)


class TestE2ETelemetryIngestion:
    """E2E: Telemetry endpoint validation."""

    def test_turn_outcome_with_valid_values(self, client):
        """Turn outcome with valid values should succeed."""
        resp = client.post(
            "/v1/telemetry/turn_outcome",
            json={
                "outcome": "SUCCESS",
                "reason_code": "success",
            },
        )
        if resp.status_code == 200:
            data = resp.json()
            assert data.get("status") in ("ok", "ignored", "accepted")

    def test_turn_outcome_with_invalid_outcome(self, client):
        """Turn outcome with invalid outcome should be ignored."""
        resp = client.post(
            "/v1/telemetry/turn_outcome",
            json={
                "outcome": "INVALID_OUTCOME_VALUE",
                "reason_code": "unknown",
            },
        )
        if resp.status_code == 200:
            data = resp.json()
            assert data.get("status") == "ignored"


class TestE2EWebhookSecurity:
    """E2E: Webhook signature verification flow."""

    def test_webhook_without_signature_rejected(self, client):
        """Webhook with no signature should be rejected."""
        resp = client.post(
            "/v1/webhooks/github",
            content=b'{"action": "test"}',
            headers={"Content-Type": "application/json"},
        )
        # Should be 401 (no sig) or 400 (missing header) or 503 (no secret configured)
        assert resp.status_code in (400, 401, 422, 503)

    def test_webhook_with_bad_signature_rejected(self, client):
        """Webhook with invalid signature should be rejected."""
        with patch("api.github_app.verify_webhook_signature", return_value=False):
            resp = client.post(
                "/v1/webhooks/github",
                content=b'{"action": "test"}',
                headers={
                    "Content-Type": "application/json",
                    "X-Hub-Signature-256": "sha256=invalid",
                    "X-GitHub-Delivery": str(uuid.uuid4()),
                    "X-GitHub-Event": "push",
                },
            )
            assert resp.status_code in (401, 503)


class TestE2EInputGuardrails:
    """E2E: Input validation and guardrails across the pipeline."""

    def test_agent_input_guard_in_source(self):
        """Agent should have a max prompt chars guard."""
        source = inspect.getsource(
            __import__("autodebug.agent.core", fromlist=["run_agent"]).run_agent
        )
        assert "_MAX_PROMPT_CHARS" in source or "50_000" in source or "50000" in source

    def test_pii_detection_patterns(self):
        """PII scrubber should detect all key patterns."""
        from worker.privacy import PII_PATTERNS

        assert "email" in PII_PATTERNS
        assert "ssn" in PII_PATTERNS
        assert "credit_card" in PII_PATTERNS

    def test_private_ip_ssrf_guard(self):
        """Auth should block private IP issuers."""
        import ipaddress

        # These should be detected as private
        for ip_str in ["10.0.0.1", "192.168.1.1", "127.0.0.1", "169.254.1.1"]:
            ip = ipaddress.ip_address(ip_str)
            assert ip.is_private or ip.is_loopback or ip.is_link_local

        # Public IP should pass
        ip = ipaddress.ip_address("8.8.8.8")
        assert not ip.is_private and not ip.is_loopback and not ip.is_link_local

    def test_ssrf_guard_in_auth_source(self):
        """Auth module should have IP check that separates parse from validation."""
        from api.auth import _validate_issuer

        source = inspect.getsource(_validate_issuer)
        # Should have separate try/except for ipaddress.ip_address parsing
        assert "ip_address" in source
        assert "is_private" in source
