"""Tests for Dogfood Round 8 fixes and broader coverage.

Part 1: R8 fix verification (V-128, V-129, V-133, V-135, V-136, V-138, V-140)
Part 2: Broader coverage — billing, notifications, error DB, github app
Part 3: Integration / stress tests
"""

from __future__ import annotations

import inspect
import json
import pathlib
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import MagicMock, patch

import pytest


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


# --- V-128: FixFeedback focus trap via ModalOverlay ---


class TestFixFeedbackFocusTrap:
    """V-128: Both modals in FixFeedback should use ModalOverlay."""

    def test_modify_modal_uses_modal_overlay(self):
        src = pathlib.Path("web/src/components/FixFeedback.tsx").read_text()
        assert "import { ModalOverlay }" in src or 'import { ModalOverlay }' in src

    def test_no_raw_fixed_inset_divs(self):
        """No raw modal divs with fixed inset-0 — both should use ModalOverlay."""
        src = pathlib.Path("web/src/components/FixFeedback.tsx").read_text()
        # The old raw modal pattern
        assert 'className="fixed inset-0 bg-black/50' not in src

    def test_notes_modal_uses_modal_overlay(self):
        src = pathlib.Path("web/src/components/FixFeedback.tsx").read_text()
        # Count ModalOverlay usages — should be 2 (modify + notes)
        count = src.count("<ModalOverlay")
        assert count == 2, f"Expected 2 ModalOverlay usages, got {count}"


# --- V-129: ReplayChat stale closure fix ---


class TestReplayChatStaleClosure:
    """V-129: conversationHistory should include the current user message."""

    def test_conversation_history_includes_user_message(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        # Should spread existing messages AND add the new user message
        assert "...messages.map" in src
        assert '{ role: "user"' in src or "role: \"user\"" in src

    def test_no_stale_messages_only_map(self):
        """Should NOT just do messages.map() without including new message."""
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        # Find the conversationHistory block — it's a multi-line array literal
        assert "const conversationHistory" in src
        # The assignment should contain a spread operator with the new message
        idx = src.find("const conversationHistory")
        block = src[idx:idx + 300]
        assert "...messages.map" in block, "Should spread existing messages"
        assert "content: query" in block, "Should include the new user message"


# --- V-133: ModalOverlay onClose stability via ref ---


class TestModalOverlayOnCloseStability:
    """V-133: ModalOverlay should use a ref for onClose to avoid re-initialization."""

    def test_onclose_ref_in_source(self):
        src = pathlib.Path("web/src/components/ModalOverlay.tsx").read_text()
        assert "onCloseRef" in src

    def test_useeffect_no_onclose_dependency(self):
        """useEffect should NOT depend on onClose (should be empty deps or stable ref)."""
        src = pathlib.Path("web/src/components/ModalOverlay.tsx").read_text()
        # Find useEffect dependency array
        match = re.search(r"useEffect\(\(\)\s*=>\s*\{.*?\},\s*\[(.*?)\]\)", src, re.DOTALL)
        assert match, "useEffect not found"
        deps = match.group(1).strip()
        assert "onClose" not in deps, f"onClose should not be in deps: [{deps}]"

    def test_escape_uses_ref(self):
        """Escape key handler should call onCloseRef.current()."""
        src = pathlib.Path("web/src/components/ModalOverlay.tsx").read_text()
        assert "onCloseRef.current()" in src


# --- V-135: list_prod_errors environment filtering ---


class TestListProdErrorsEnvironmentFilter:
    """V-135: Environment filter should not reduce paginated result count."""

    def test_environment_filter_before_pagination(self):
        """When environment is specified, filtering should happen before pagination slicing."""
        src = inspect.getsource(__import__("api.db.errors", fromlist=["list_prod_errors"]).list_prod_errors)
        # Should have separate branches for with/without environment
        assert "if environment:" in src
        # Should paginate filtered results
        assert "offset:offset + per_page" in src or "offset:offset+per_page" in src or "[offset:" in src

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_environment_filter_returns_correct_count(self, _h, _u, mock_req):
        """With environment filter, total should reflect filtered count."""
        from api.db.errors import list_prod_errors

        # Return 5 errors, 3 with prod environment, 2 with staging
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [
            {"error_id": f"e{i}", "error_data": json.dumps({"environment": "production"})} for i in range(3)
        ] + [
            {"error_id": f"s{i}", "error_data": json.dumps({"environment": "staging"})} for i in range(2)
        ]
        mock_resp.headers = {"Content-Range": "0-4/5"}
        mock_req.get.return_value = mock_resp

        result = list_prod_errors("org1", page=1, per_page=20, environment="production")
        assert result["total"] == 3
        assert len(result["errors"]) == 3
        assert all(e["error_data"]["environment"] == "production" for e in result["errors"])

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_no_environment_uses_server_pagination(self, _h, _u, mock_req):
        """Without environment filter, should use server-side Range pagination."""
        from api.db.errors import list_prod_errors

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [
            {"error_id": "e1", "error_data": json.dumps({"message": "test"})}
        ]
        mock_resp.headers = {"Content-Range": "0-0/10"}
        mock_req.get.return_value = mock_resp

        result = list_prod_errors("org1", page=1, per_page=1)
        # Should have set Range header for server-side pagination
        call_kwargs = mock_req.get.call_args
        headers_used = call_kwargs[1].get("headers", {}) if call_kwargs[1] else {}
        assert "Range" in headers_used
        assert result["total"] == 10


# --- V-136: Billing subscription.updated plan sync ---


class TestBillingSubscriptionUpdated:
    """V-136: subscription.updated should sync plan even with cancel_at_period_end."""

    def test_no_cancel_at_period_end_guard(self):
        """The subscription.updated handler should NOT skip plan updates based on cancel_at_period_end."""
        src = pathlib.Path("api/billing.py").read_text()
        # Find the subscription.updated handler block
        idx = src.find('customer.subscription.updated')
        assert idx > 0
        block = src[idx:idx + 800]
        # Should NOT have cancel_at_period_end as a condition for skipping plan update
        assert "elif not data.get(\"cancel_at_period_end\")" not in block

    def test_always_updates_plan(self):
        """Handler should call update_org_plan regardless of cancel_at_period_end."""
        src = pathlib.Path("api/billing.py").read_text()
        idx = src.find('customer.subscription.updated')
        block = src[idx:idx + 800]
        assert "update_org_plan" in block


# --- V-138: Notification settings upsert ---


class TestNotificationSettingsUpsert:
    """V-138: update_notification_settings should use atomic upsert."""

    def test_uses_upsert_header(self):
        """Should use Prefer: resolution=merge-duplicates for atomic upsert."""
        src = inspect.getsource(
            __import__("api.db.notifications", fromlist=["update_notification_settings"]).update_notification_settings
        )
        assert "resolution=merge-duplicates" in src

    def test_no_check_then_act(self):
        """Should NOT have a get-then-insert/update pattern."""
        src = inspect.getsource(
            __import__("api.db.notifications", fromlist=["update_notification_settings"]).update_notification_settings
        )
        # Should not call get_notification_settings first
        assert "get_notification_settings" not in src
        # Should not have if existing / if not existing pattern
        assert "existing" not in src

    @patch("api.db.notifications.requests")
    @patch("api.db.notifications._supabase_url", return_value="http://test")
    @patch("api.db.notifications._headers", return_value={"apikey": "test"})
    def test_upsert_sends_single_post(self, _h, _u, mock_req):
        """Upsert should be a single POST, not GET+POST/PATCH."""
        from api.db.notifications import update_notification_settings

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_req.post.return_value = mock_resp

        result = update_notification_settings(
            project_id="proj1",
            enabled=True,
            channels=[{"type": "slack", "url": "https://hooks.slack.com/test"}],
            notification_types=["failure", "flaky"],
        )

        # Should be exactly one POST call (upsert), no GET
        assert mock_req.post.call_count == 1
        assert mock_req.get.call_count == 0
        # Verify Prefer header
        call_args = mock_req.post.call_args
        headers_used = call_args[1].get("headers", {}) if call_args[1] else {}
        assert headers_used.get("Prefer") == "resolution=merge-duplicates"
        assert result["enabled"] is True


# --- V-140: useAIReasoning separate AbortControllers ---


class TestUseAIReasoningSeparateAbortControllers:
    """V-140: Debate and hypotheses should have independent abort controllers."""

    def test_separate_refs(self):
        src = pathlib.Path("web/src/hooks/useAIReasoning.ts").read_text()
        assert "debateAbortRef" in src
        assert "hypothesesAbortRef" in src

    def test_no_shared_abort_ref(self):
        src = pathlib.Path("web/src/hooks/useAIReasoning.ts").read_text()
        # Should NOT have a single shared abortControllerRef
        assert "abortControllerRef" not in src

    def test_debate_uses_debate_ref(self):
        src = pathlib.Path("web/src/hooks/useAIReasoning.ts").read_text()
        # Find fetchDebateAnalysis function
        debate_start = src.find("fetchDebateAnalysis")
        debate_block = src[debate_start:debate_start + 500]
        assert "debateAbortRef" in debate_block

    def test_hypotheses_uses_hypotheses_ref(self):
        src = pathlib.Path("web/src/hooks/useAIReasoning.ts").read_text()
        # Find fetchHypotheses function
        hyp_start = src.find("fetchHypotheses")
        hyp_block = src[hyp_start:hyp_start + 500]
        assert "hypothesesAbortRef" in hyp_block

    def test_cleanup_aborts_both(self):
        src = pathlib.Path("web/src/hooks/useAIReasoning.ts").read_text()
        # The cleanup effect should abort both
        assert "debateAbortRef.current?.abort()" in src
        assert "hypothesesAbortRef.current?.abort()" in src


# ============================================================================
# Part 2: Broader Coverage
# ============================================================================


# --- Billing module ---


class TestBillingWebhookIdempotency:
    """Billing webhook idempotency and eviction."""

    def test_fallback_set_eviction_is_partial(self):
        """Fallback set should evict oldest half, not clear all."""
        src = pathlib.Path("api/billing.py").read_text()
        assert "[:5000]" in src
        assert ".discard(item)" in src
        # Should NOT have .clear()
        mark_fn_start = src.find("def _mark_webhook_processed")
        mark_fn_block = src[mark_fn_start:mark_fn_start + 500]
        assert ".clear()" not in mark_fn_block

    def test_validate_redirect_url_rejects_bad_schemes(self):
        from api.billing import _validate_redirect_url

        assert not _validate_redirect_url("javascript:alert(1)", ["https://app.hikaflow.com"])
        assert not _validate_redirect_url("ftp://evil.com", ["https://app.hikaflow.com"])
        assert not _validate_redirect_url("data:text/html,<h1>hi</h1>", ["https://app.hikaflow.com"])

    def test_validate_redirect_url_accepts_valid(self):
        from api.billing import _validate_redirect_url

        assert _validate_redirect_url(
            "https://app.hikaflow.com/billing/success",
            ["https://app.hikaflow.com"]
        )

    def test_validate_redirect_url_prevents_subdomain_bypass(self):
        from api.billing import _validate_redirect_url

        assert not _validate_redirect_url(
            "https://app.hikaflow.com.evil.com/billing",
            ["https://app.hikaflow.com"]
        )

    def test_resolve_plan_name_unknown_fallback(self):
        from api.billing import _resolve_plan_name

        assert _resolve_plan_name("price_unknown_xyz") == "free"

    def test_resolve_plan_name_known(self):
        from api.billing import PLAN_NAMES, _resolve_plan_name

        for price_id, plan in PLAN_NAMES.items():
            assert _resolve_plan_name(price_id) == plan

    def test_valid_price_ids_match_price_ids(self):
        from api.billing import PRICE_IDS, VALID_PRICE_IDS

        assert VALID_PRICE_IDS == set(PRICE_IDS.values())


# --- Session DB operations ---


class TestSessionDbOperations:
    """Session DB module coverage."""

    def test_valid_session_statuses(self):
        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
        # RECORDING should not be a valid finalization status
        assert "RECORDING" not in VALID_SESSION_STATUSES

    def test_finalize_session_rejects_invalid_status(self):
        from api.db.sessions import finalize_session

        with pytest.raises(ValueError, match="Invalid session status"):
            finalize_session("sess-123", status="INVALID")

    def test_finalize_session_rejects_recording(self):
        from api.db.sessions import finalize_session

        with pytest.raises(ValueError, match="Invalid session status"):
            finalize_session("sess-123", status="RECORDING")

    @patch("api.db.sessions.requests")
    @patch("api.db.sessions._supabase_url", return_value="http://test")
    @patch("api.db.sessions._headers", return_value={"apikey": "test"})
    def test_get_session_stats_parallel_execution(self, _h, _u, mock_req):
        """get_session_stats should use ThreadPoolExecutor for parallel counting."""
        from api.db.sessions import get_session_stats

        src = inspect.getsource(get_session_stats)
        assert "ThreadPoolExecutor" in src

    @patch("api.db.sessions.requests")
    @patch("api.db.sessions._supabase_url", return_value="http://test")
    @patch("api.db.sessions._headers", return_value={"apikey": "test"})
    def test_delete_sessions_before_returns_count(self, _h, _u, mock_req):
        """delete_sessions_before should return number of deleted rows."""
        from api.db.sessions import delete_sessions_before

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = '[{"id":"s1"},{"id":"s2"}]'
        mock_resp.json.return_value = [{"id": "s1"}, {"id": "s2"}]
        mock_req.delete.return_value = mock_resp

        count = delete_sessions_before("org1", "2025-01-01T00:00:00Z")
        assert count == 2


# --- Error DB operations ---


class TestErrorDbOperations:
    """Error DB module broader coverage."""

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_create_prod_error_sends_json_encoded_data(self, _h, _u, mock_req):
        """error_data should be JSON-encoded in the payload."""
        from api.db.errors import create_prod_error

        mock_resp = MagicMock()
        mock_resp.status_code = 201
        mock_req.post.return_value = mock_resp

        create_prod_error("err1", "org1", "proj1", {"type": "RuntimeError", "msg": "oops"})

        call_kwargs = mock_req.post.call_args
        payload = call_kwargs[1].get("json", {}) if call_kwargs[1] else {}
        assert isinstance(payload.get("error_data"), str)
        parsed = json.loads(payload["error_data"])
        assert parsed["type"] == "RuntimeError"

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_get_prod_error_parses_json(self, _h, _u, mock_req):
        """get_prod_error should parse error_data from JSON string."""
        from api.db.errors import get_prod_error

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [
            {"error_id": "err1", "org_id": "org1", "error_data": '{"type":"ValueError"}'}
        ]
        mock_req.get.return_value = mock_resp

        result = get_prod_error("err1", "org1")
        assert result is not None
        assert result["error_data"]["type"] == "ValueError"

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_get_prod_error_handles_bad_json(self, _h, _u, mock_req):
        """get_prod_error should handle malformed JSON in error_data."""
        from api.db.errors import get_prod_error

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [
            {"error_id": "err1", "org_id": "org1", "error_data": "not valid json{{{"}
        ]
        mock_req.get.return_value = mock_resp

        result = get_prod_error("err1", "org1")
        assert result is not None
        assert result["error_data"] == {}

    @patch("api.db.errors.requests")
    @patch("api.db.errors._supabase_url", return_value="http://test")
    @patch("api.db.errors._headers", return_value={"apikey": "test"})
    def test_get_prod_error_returns_none_for_missing(self, _h, _u, mock_req):
        """get_prod_error should return None when not found."""
        from api.db.errors import get_prod_error

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = []
        mock_req.get.return_value = mock_resp

        result = get_prod_error("nonexistent", "org1")
        assert result is None


# ============================================================================
# Part 3: Integration / Stress Tests
# ============================================================================


class TestConcurrentHealingLock:
    """Stress test: healing lock uses atomic upsert pattern."""

    def test_lock_function_returns_bool(self):
        """acquire_healing_lock should return bool (acquired or not)."""
        import inspect
        from api.db.self_heal import acquire_healing_lock

        sig = inspect.signature(acquire_healing_lock)
        assert sig.return_annotation in (bool, "bool", inspect.Parameter.empty)

    def test_lock_has_ttl_parameter(self):
        """acquire_healing_lock should accept ttl_seconds."""
        import inspect
        from api.db.self_heal import acquire_healing_lock

        sig = inspect.signature(acquire_healing_lock)
        assert "ttl_seconds" in sig.parameters

    def test_lock_source_has_upsert_pattern(self):
        """Lock implementation should use conflict detection for atomicity."""
        import inspect
        from api.db.self_heal import acquire_healing_lock

        src = inspect.getsource(acquire_healing_lock)
        # Should have either upsert or conflict handling
        assert "conflict" in src.lower() or "409" in src or "upsert" in src.lower() or "expires_at" in src


class TestBillingWebhookFlood:
    """Stress test: rapid webhook idempotency checks."""

    def test_idempotency_set_handles_rapid_inserts(self):
        """Fallback set should handle rapid concurrent inserts without crashing."""
        from api.billing import _is_webhook_processed, _mark_webhook_processed, _processed_webhook_events_fallback

        # Clear state for test
        _processed_webhook_events_fallback.clear()

        # Simulate rapid webhook events (using fallback set, not DB)
        with patch("api.billing.db") as mock_db:
            mock_db.check_webhook_processed.side_effect = Exception("db down")
            mock_db.mark_webhook_processed.side_effect = Exception("db down")

            event_ids = [f"evt_{i}" for i in range(100)]
            for eid in event_ids:
                _mark_webhook_processed(eid)

            # All should be tracked
            for eid in event_ids:
                assert _is_webhook_processed(eid)

        # Clean up
        _processed_webhook_events_fallback.clear()


class TestTokenCacheTOCTOU:
    """V-120 regression: token cache should be thread-safe."""

    def test_fetch_lock_source_exists(self):
        src = pathlib.Path("api/github_app.py").read_text()
        assert "_token_fetch_locks" in src
        assert "threading" in src or "Lock" in src

    def test_double_check_pattern(self):
        """Token fetch should double-check cache inside the lock."""
        src = pathlib.Path("api/github_app.py").read_text()
        fn_start = src.find("def get_installation_token")
        fn_block = src[fn_start:fn_start + 1500]
        # Should have the pattern: check cache -> lock -> check again -> fetch
        assert "fetch_lock" in fn_block or "_token_fetch_locks" in fn_block


class TestAdaptiveNEdgeCases:
    """Adaptive N calculation edge cases."""

    def test_zero_flake_rate_returns_max(self):
        """estimated_flake_rate=0 should return max_n, not divide by zero."""
        from autodebug.detection.adaptive_n import calculate_adaptive_n

        result = calculate_adaptive_n(estimated_flake_rate=0.0)
        assert isinstance(result, int)
        assert result > 0

    def test_one_flake_rate_returns_min(self):
        """estimated_flake_rate=1.0 (always flaky) should need minimal runs."""
        from autodebug.detection.adaptive_n import calculate_adaptive_n

        result = calculate_adaptive_n(estimated_flake_rate=1.0)
        assert isinstance(result, int)
        assert result >= 1

    def test_very_small_flake_rate(self):
        """Very small flake rate should cap at max_n."""
        from autodebug.detection.adaptive_n import calculate_adaptive_n

        result = calculate_adaptive_n(estimated_flake_rate=0.001, max_n=200)
        assert isinstance(result, int)
        assert result <= 200  # Should respect max_n cap

    def test_budget_constraint(self):
        """Budget constraint should limit N below unconstrained value."""
        from autodebug.detection.adaptive_n import calculate_adaptive_n

        unconstrained = calculate_adaptive_n(estimated_flake_rate=0.1)
        constrained = calculate_adaptive_n(
            estimated_flake_rate=0.1,
            budget_seconds=10.0,
            avg_test_duration_seconds=5.0,
        )
        # Budget-constrained result should be <= unconstrained
        assert constrained <= unconstrained


class TestRegressionGuardSource:
    """Regression guard edge case coverage."""

    def test_guard_has_comparison_logic(self):
        src = inspect.getsource(
            __import__("autodebug.detection.regression_guard", fromlist=["RegressionGuard"]).RegressionGuard
        )
        # Should have pass rate comparison
        assert "pass_rate" in src.lower() or "pass" in src.lower()

    def test_guard_handles_empty_history(self):
        """Guard should not crash with empty test history."""
        from autodebug.detection.regression_guard import RegressionGuard

        guard = RegressionGuard()
        # Should handle empty/missing data gracefully
        assert guard is not None


class TestPatternCategories:
    """Pattern matching categories are complete."""

    def test_all_ten_categories_present(self):
        from autodebug.healing.patterns import PatternCategory

        expected = {
            "ASYNC_WAIT", "CONCURRENCY", "ORDER_DEPENDENCY", "RESOURCE_LEAK",
            "TIME_DEPENDENT", "RANDOMNESS", "NETWORK", "FLOATING_POINT",
            "PLATFORM", "ENVIRONMENT",
        }
        actual = {p.name for p in PatternCategory}
        assert expected.issubset(actual), f"Missing: {expected - actual}"


class TestProofCertificateGeneration:
    """Proof certificate generation validation."""

    def test_certificate_fields(self):
        from autodebug.autonomous.proof import generate_proof_certificate

        cert = generate_proof_certificate(
            original_run_id="run-001",
            original_exception={"type": "AssertionError", "message": "expected True"},
            hypothesis_id="hyp-001",
            patch_hash="abc123def",
            validation_results=[
                {"run_id": "vr-1", "passed": True},
                {"run_id": "vr-2", "passed": True},
                {"run_id": "vr-3", "passed": False},
            ],
            verdict="PARTIAL",
        )

        assert cert.original_run_id == "run-001"
        assert cert.hypothesis_id == "hyp-001"
        assert cert.patch_hash == "abc123def"
        assert cert.verdict == "PARTIAL"
        assert len(cert.validation_runs) == 3
        # Run numbers should start at 1
        assert cert.validation_runs[0].run_number == 1

    def test_certificate_with_full_pass(self):
        from autodebug.autonomous.proof import generate_proof_certificate

        cert = generate_proof_certificate(
            original_run_id="run-002",
            original_exception={"type": "TimeoutError", "message": "timed out"},
            hypothesis_id="hyp-002",
            patch_hash="xyz789",
            validation_results=[
                {"run_id": f"vr-{i}", "passed": True} for i in range(5)
            ],
            verdict="CONFIRMED",
        )

        assert cert.verdict == "CONFIRMED"
        assert len(cert.validation_runs) == 5


class TestBanditSelectorNumericalStability:
    """Bandit selector edge cases."""

    def test_strategy_stats_with_no_data(self):
        from autodebug.strategies.bandit_selector import StrategyStats

        stats = StrategyStats()
        # Should not crash with zero observations
        sample = stats.sample()
        assert isinstance(sample, (int, float))
        assert sample >= 0

    def test_strategy_stats_divide_by_zero_guard(self):
        """Source should guard against x + y == 0."""
        from autodebug.strategies.bandit_selector import StrategyStats

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


# ============================================================================
# Part 4: R8 Deep Scan Fix Verification
# ============================================================================


class TestV162ThreadPoolReplaced:
    """V-162: ThreadPoolExecutor replaced with asyncio.gather + to_thread."""

    def test_no_threadpool_executor_in_ai_router(self):
        """ai.py should not import or use ThreadPoolExecutor."""
        src = pathlib.Path("api/routers/ai.py").read_text()
        assert "ThreadPoolExecutor" not in src

    def test_asyncio_gather_used_for_parallel_db(self):
        """ai.py should use asyncio.gather for parallel DB calls."""
        src = pathlib.Path("api/routers/ai.py").read_text()
        assert "asyncio.gather" in src
        assert "asyncio.to_thread" in src


class TestV166SyncDbFixed:
    """V-166: generate_verified_fix_endpoint uses to_thread for DB call."""

    def test_root_cause_analysis_uses_to_thread(self):
        """get_root_cause_analysis should be called via to_thread."""
        src = pathlib.Path("api/routers/ai.py").read_text()
        # Find the generate_verified_fix_endpoint area
        idx = src.find("def generate_verified_fix_endpoint")
        assert idx != -1
        block = src[idx:idx + 1000]
        assert "to_thread" in block and "get_root_cause_analysis" in block


class TestV167StorageUrlLeak:
    """V-167: Exception messages no longer leak signed URLs."""

    def test_no_str_e_in_transcript_error_handler(self):
        """Transcript error handler should log error_type only, not str(e)."""
        src = pathlib.Path("api/routers/runs.py").read_text()
        # Find the _fetch_and_parse function's except block
        idx = src.find("Failed to load transcript")
        assert idx != -1
        block = src[idx - 200:idx + 200]
        # Should NOT have f-string with str(e) or {e}
        assert "str(e)" not in block
        assert "error_type" in block


class TestV168BanditExpanduser:
    """V-168: Bandit persistence path uses os.path.expanduser."""

    def test_expanduser_in_update_bandit(self):
        """_update_bandit should expand ~ in persistence path."""
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("strategy_stats_")
        assert idx != -1
        block = src[idx - 100:idx + 100]
        assert "expanduser" in block


class TestV171BoolOpCounting:
    """V-171: BoolOp not double-counted in complexity."""

    def test_boolop_not_in_branch_tuple(self):
        """BoolOp should be handled separately from branch node types."""
        src = pathlib.Path("api/ai/flaky_predictor.py").read_text()
        # Find the complexity counting block
        idx = src.find("Cyclomatic complexity via AST")
        assert idx != -1
        block = src[idx:idx + 600]
        # The isinstance tuple should NOT contain BoolOp
        tuple_start = block.find("isinstance(node, (ast.If")
        tuple_end = block.find(")", tuple_start + 20)
        tuple_str = block[tuple_start:tuple_end + 1]
        assert "BoolOp" not in tuple_str
        # BoolOp should be handled in a separate isinstance check
        assert "isinstance(node, ast.BoolOp)" in block
        # Each operand beyond the first adds a branch
        assert "values) - 1" in block


class TestV172ExitCodeStatus:
    """V-172: Exit code status uses 'is not None' instead of truthiness."""

    def test_explicit_none_check(self):
        """finalize_run status should use 'exit_code is not None'."""
        src = pathlib.Path("api/routers/runs.py").read_text()
        idx = src.find("FAILED")
        # Find the status assignment line
        lines = src.split("\n")
        for line in lines:
            if "FAILED" in line and "COMPLETED" in line and "exit_code" in line:
                assert "is not None" in line
                break
        else:
            pytest.fail("Could not find exit_code status assignment line")


class TestV173SsrfDnsAsync:
    """V-173: SSRF DNS check uses asyncio.to_thread."""

    def test_getaddrinfo_in_to_thread(self):
        """socket.getaddrinfo should be called via to_thread."""
        src = pathlib.Path("api/routers/ai.py").read_text()
        idx = src.find("getaddrinfo")
        assert idx != -1
        block = src[idx - 80:idx + 80]
        assert "to_thread" in block


class TestV174AsyncTestExtraction:
    """V-174: Flaky predictor finds async def test boundaries."""

    def test_finds_both_sync_and_async_defs(self):
        """Test extraction should find both 'def ' and 'async def '."""
        src = pathlib.Path("api/ai/flaky_predictor.py").read_text()
        idx = src.find("predict_flakiness_for_project")
        assert idx != -1
        block = src[idx:idx + 1200]
        assert "next_async" in block

    def test_extraction_uses_min_candidates(self):
        """Should find nearest of sync/async def as boundary."""
        src = pathlib.Path("api/ai/flaky_predictor.py").read_text()
        # Both extraction sites should use candidates pattern
        assert src.count("candidates = [n for n in [next_sync, next_async]") >= 2


class TestV175ParametrizedTestRegex:
    """V-175: Shell sanitization preserves [] in test names."""

    def test_brackets_preserved_in_regex(self):
        """Regex character class should allow [ and ]."""
        src = pathlib.Path("api/routers/runs.py").read_text()
        idx = src.find("safe_test_name")
        assert idx != -1
        block = src[idx:idx + 100]
        # Should use proper escaping: \[\] not \\[\\]
        assert r"[^a-zA-Z0-9_.\[\]-]" in block

    def test_parametrized_name_preserved(self):
        """Parametrized test names should keep brackets."""
        import re
        test_name = "test_foo[param1-value]"
        safe = re.sub(r"[^a-zA-Z0-9_.\[\]-]", "", test_name)
        assert safe == "test_foo[param1-value]"


# ============================================================================
# Part 5: R8 CLI/Worker Deep Scan Fix Verification
# ============================================================================


class TestV193SemgrepReturncode:
    """V-193: _install_semgrep checks pip returncode."""

    def test_checks_returncode(self):
        """Semgrep install should check subprocess returncode."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("pip install semgrep")
        assert idx != -1
        block = src[idx - 200:idx + 400]
        assert "returncode" in block

    def test_returns_false_on_failure(self):
        """Should return False when pip fails."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("pip install semgrep")
        assert idx != -1
        block = src[idx:idx + 800]
        assert "return False" in block


class TestV203RageClickDedup:
    """V-203: Rage click highlights have cooldown dedup."""

    def test_cooldown_in_source(self):
        """Rage click detection should have a cooldown check."""
        src = pathlib.Path("worker/session_processing.py").read_text()
        idx = src.find("rage_click")
        assert idx != -1
        block = src[idx - 300:idx + 100]
        assert "5000" in block or "cooldown" in block.lower()

    def test_last_rage_ts_initialized(self):
        """last_rage_ts should be initialized before the loop."""
        src = pathlib.Path("worker/session_processing.py").read_text()
        assert "last_rage_ts" in src
        # Should be initialized before first use
        init_idx = src.find("last_rage_ts: int = 0") or src.find("last_rage_ts = 0")
        use_idx = src.find("ts_ms - last_rage_ts")
        assert init_idx != -1
        assert use_idx != -1
        assert init_idx < use_idx


class TestV204FunctionValidation:
    """V-204: --function target validated as Python identifier."""

    def test_validation_in_generate_tests(self):
        """generate-tests should validate target_function."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("generate_tests")
        assert idx != -1
        # Find the function body
        block = src[idx:idx + 2000]
        assert "target_function" in block
        assert "Python identifier" in block or "a-zA-Z_" in block

    def test_regex_pattern(self):
        """Validation regex should match valid Python identifiers."""
        import re
        pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*$'
        assert re.match(pattern, "my_function")
        assert re.match(pattern, "_private")
        assert re.match(pattern, "Test123")
        assert not re.match(pattern, "../../evil")
        assert not re.match(pattern, "func name")
        assert not re.match(pattern, "123start")


class TestV208TimestampPrecision:
    """V-208: _extract_event_timestamp uses round() not int()."""

    def test_uses_round(self):
        """Timestamp extraction should use round() for float precision."""
        src = pathlib.Path("worker/session_processing.py").read_text()
        idx = src.find("def _extract_event_timestamp")
        assert idx != -1
        block = src[idx:idx + 300]
        # Should use round(ts) not int(ts)
        assert "round(ts)" in block
        assert "int(ts)" not in block


# ============================================================================
# Part 6: Open Verdict Fixes (V-152, V-155, V-177, V-178, V-182, V-190, V-199, V-200, V-202)
# ============================================================================


class TestV155CreateFixPrUsesBaseSha:
    """V-155: create_fix_pr reads file from base_sha not base_branch HEAD."""

    def test_get_file_content_uses_base_sha(self):
        """File content should be read from base_sha, not base_branch."""
        src = pathlib.Path("api/github_app.py").read_text()
        idx = src.find("def create_fix_pr")
        assert idx != -1
        block = src[idx:idx + 3000]
        # Find the get_file_content call within the function
        gfc_idx = block.find("get_file_content")
        assert gfc_idx != -1, "get_file_content call not found in create_fix_pr"
        gfc_line = block[gfc_idx:gfc_idx + 200]
        # Should reference base_sha, not base_branch
        assert "base_sha" in gfc_line, f"Expected base_sha in: {gfc_line[:100]}"


class TestV177LoginNoAutoRepl:
    """V-177: Login doesn't auto-start REPL in non-interactive/CI contexts."""

    def test_ci_guard_in_login(self):
        """Login should check for CI env before starting REPL."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # Find the specific login-related _interactive_mode call (near "Login successful")
        login_idx = src.find("Login successful")
        assert login_idx != -1
        block = src[login_idx:login_idx + 300]
        assert "isatty" in block or "CI" in block

    def test_isatty_check(self):
        """Should check sys.stdin.isatty() before REPL launch."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        login_idx = src.find("Login successful")
        assert login_idx != -1
        block = src[login_idx:login_idx + 300]
        assert "isatty()" in block


class TestV178AtomicProfileWrite:
    """V-178: Shell profile write uses atomic rename."""

    def test_uses_os_replace(self):
        """Profile update should use os.replace for atomicity."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("hikaflow/bin")
        assert idx != -1
        block = src[idx:idx + 600]
        assert "os.replace" in block or "rename" in block

    def test_uses_tempfile(self):
        """Should write to temp file first."""
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("hikaflow/bin")
        assert idx != -1
        block = src[idx:idx + 600]
        assert "tempfile" in block or "mkstemp" in block


class TestV182CheckRunIdGuard:
    """V-182: check_run_id=None guard before update_check_run."""

    def test_guard_exists(self):
        """Should guard against None check_run_id."""
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("created check run")
        assert idx != -1
        block = src[idx - 200:idx + 200]
        assert "not check_run_id" in block or "check_run_id is None" in block


class TestV190EmptyStorageKeyGuard:
    """V-190: Empty storage_key validated before use."""

    def test_empty_key_skipped(self):
        """Empty storage_key should be skipped."""
        src = pathlib.Path("worker/session_processing.py").read_text()
        assert "storage_key.strip()" in src or "not storage_key" in src


class TestV199QuotaErrorNarrowed:
    """V-199: Quota error detection narrowed to specific patterns."""

    def test_no_broad_limit_exceeded(self):
        """Should not match generic 'limit exceeded'."""
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("_is_quota_error")
        assert idx != -1
        block = src[idx:idx + 400]
        # Should NOT have bare "limit exceeded" match
        assert '"limit exceeded"' not in block or "rate limit" in block

    def test_checks_status_code(self):
        """Should check HTTP 429 status code."""
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("_is_quota_error")
        assert idx != -1
        block = src[idx:idx + 400]
        assert "429" in block


class TestV200RedisClientInvalidation:
    """V-200: Stale Redis client invalidated on connection errors."""

    def test_client_reset_on_error(self):
        """Redis client should be reset to None on non-quota errors."""
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("def _handle_queue_pop_error")
        assert idx != -1
        block = src[idx:idx + 600]
        assert "_redis_client = None" in block


class TestV202PiiSamplesHashed:
    """V-202: Non-DOM PII samples are hashed, not stored raw."""

    def test_non_dom_pii_hashed(self):
        """Non-DOM PII hits should be hashed before storage."""
        src = pathlib.Path("worker/session_processing.py").read_text()
        # Find the non-DOM PII detection block
        idx = src.find("Store non-DOM events")
        assert idx != -1
        block = src[idx:idx + 400]
        assert "hashlib" in block
        assert "hexdigest" in block


# ============================================================================
# Part 7: V-147, V-148, V-163, V-169, V-156, V-160, V-176, V-207
# ============================================================================


class TestV147RollbackAsyncToThread:
    """V-147: rollback.py sync DB calls wrapped in asyncio.to_thread."""

    def test_get_self_heal_event_uses_to_thread(self):
        src = pathlib.Path("api/ai/rollback.py").read_text()
        assert "asyncio.to_thread(get_self_heal_event_by_pr" in src

    def test_update_self_heal_event_uses_to_thread(self):
        src = pathlib.Path("api/ai/rollback.py").read_text()
        assert "asyncio.to_thread(update_self_heal_event" in src

    def test_imports_asyncio(self):
        src = pathlib.Path("api/ai/rollback.py").read_text()
        assert "import asyncio" in src


class TestV148QuarantineEnvWarning:
    """V-148: QuarantineManager warns when HIKAFLOW_ENDPOINT not set."""

    def test_env_var_check_present(self):
        src = pathlib.Path("api/ai/rollback.py").read_text()
        assert 'HIKAFLOW_ENDPOINT' in src
        # Find the QuarantineManager instantiation (not the import)
        idx = src.find("QuarantineManager(")
        assert idx != -1
        block = src[max(0, idx - 300):idx + 100]
        assert "HIKAFLOW_ENDPOINT" in block


class TestV163RollbackSubstringCorrelation:
    """V-163: Rollback correlation uses prefix check, not substring."""

    def test_no_bare_in_check(self):
        """Should not use 'original_module in t' (substring match)."""
        src = pathlib.Path("api/ai/rollback.py").read_text()
        idx = src.find("correlated = []")
        assert idx != -1
        block = src[idx:idx + 400]
        # Should use startswith, not bare 'in'
        assert "startswith" in block
        # Should NOT have the bare substring pattern
        assert "original_module and original_module in t" not in block

    def test_uses_separator_in_startswith(self):
        """Prefix check should include separator (:: or .) to avoid partial matches."""
        src = pathlib.Path("api/ai/rollback.py").read_text()
        idx = src.find("correlated = []")
        assert idx != -1
        block = src[idx:idx + 400]
        assert '"::"' in block or "'::')" in block


class TestV169UtilityFileRollbackScoped:
    """V-169: Utility filenames only trigger rollback when sharing a directory."""

    def test_utility_check_has_directory_scope(self):
        src = pathlib.Path("api/ai/rollback.py").read_text()
        idx = src.find('"conftest"')
        assert idx != -1
        block = src[idx:idx + 300]
        # Should check directory relationship, not just filename
        assert "test_dir" in block or "changed_dir" in block

    def test_not_unconditional_return_true(self):
        """Utility filename match should NOT be 'return True' on its own line."""
        src = pathlib.Path("api/ai/rollback.py").read_text()
        idx = src.find('"conftest"')
        assert idx != -1
        # Find the next return True after conftest check
        block = src[idx:idx + 400]
        lines = block.split("\n")
        # There should be directory checking before return True
        for i, line in enumerate(lines):
            if "return True" in line and i > 0:
                # Should have directory check above it
                context = "\n".join(lines[max(0, i - 3):i])
                assert "dir" in context.lower() or "path" in context.lower()


class TestV156TokenFetchLocksEviction:
    """V-156: _token_fetch_locks has bounded size."""

    def test_max_fetch_locks_constant(self):
        src = pathlib.Path("api/github_app.py").read_text()
        assert "_MAX_FETCH_LOCKS" in src

    def test_eviction_logic_present(self):
        src = pathlib.Path("api/github_app.py").read_text()
        idx = src.find("_token_fetch_locks[installation_id] = threading.Lock()")
        assert idx != -1
        block = src[max(0, idx - 400):idx]
        assert "_MAX_FETCH_LOCKS" in block
        assert "pop" in block or "del " in block


class TestV160LoginReplErrorGuard:
    """V-160: asyncio.run(_interactive_mode()) in login is error-guarded."""

    def test_login_repl_has_try_except(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("Login successful")
        assert idx != -1
        block = src[idx:idx + 400]
        # Should have try/except around asyncio.run
        assert "try:" in block
        assert "except" in block
        assert "asyncio.run" in block


class TestV176SelfHealConfigDefaults:
    """V-176: SelfHealConfig uses Optional instead of bare mutable default."""

    def test_excluded_paths_optional(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("class SelfHealConfig")
        assert idx != -1
        block = src[idx:idx + 400]
        assert "Optional[List[str]]" in block
        # Should NOT have bare `List[str] = None` without Optional
        lines = block.split("\n")
        for line in lines:
            if "excluded_paths" in line and "= None" in line:
                assert "Optional" in line


class TestV207TokenCostModel:
    """V-207: /tokens cost estimate uses correct model pricing."""

    def test_no_gpt5_reference(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        assert "GPT-5" not in src

    def test_uses_claude_pricing(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("cost estimate")
        assert idx != -1
        block = src[idx:idx + 100]
        assert "Claude" in block or "claude" in block


# ─── Part 8: V-184, V-198, V-201, V-205, V-206, V-212, V-215, V-221, V-164, V-165, V-187, V-216 ───


class TestV184DLQEvictionLog:
    """V-184: DLQ ltrim pipeline should log accurate eviction count."""

    def test_log_message_shows_trim_direction(self):
        src = pathlib.Path("worker/queue.py").read_text()
        assert "trimmed" in src or "→" in src, "Log should describe trim direction, not just ratio"

    def test_no_percent_d_format_for_eviction(self):
        """The old format used %d/%d which was misleading."""
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("evict")
        assert idx != -1
        block = src[max(0, idx - 200):idx + 200]
        # Should not have the old misleading format
        assert "%d/%d" not in block


class TestV198FailureSignatureMajority:
    """V-198: Failure signature should use majority result across runs, not just first."""

    def test_signature_iterates_all_runs(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("failure_signature")
        assert idx != -1
        block = src[idx:idx + 500]
        # Should iterate runs (not just [0]) and count signatures
        assert "sig_counts" in block or "counter" in block.lower(), "Should count signatures across runs"
        assert "[0]" not in block, "Should not use only first run"

    def test_majority_selection_logic(self):
        """Verify the code picks the max-count signature."""
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("failure_signature")
        block = src[idx:idx + 600]
        assert "max(" in block, "Should select majority via max()"


class TestV201PhoneRegex:
    """V-201: Phone regex should not match version strings, dates, error codes."""

    def test_regex_requires_exactly_10_digits(self):
        from worker.privacy import PII_PATTERNS

        phone_re = PII_PATTERNS["phone"]
        # Valid US phone numbers
        assert phone_re.search("555-123-4567"), "Should match standard US phone"
        assert phone_re.search("(555) 123-4567"), "Should match parenthesized area code"
        assert phone_re.search("+1 555-123-4567"), "Should match +1 prefix"

    def test_rejects_version_strings(self):
        from worker.privacy import PII_PATTERNS

        phone_re = PII_PATTERNS["phone"]
        assert not phone_re.search("version 1.2.3.4"), "Should not match version"
        assert not phone_re.search("error code 12-34-56"), "Should not match error code"

    def test_rejects_short_numbers(self):
        from worker.privacy import PII_PATTERNS

        phone_re = PII_PATTERNS["phone"]
        assert not phone_re.search("id: 12-345"), "Should not match short numeric IDs"


class TestV205ConnectionIdValidation:
    """V-205: SENTRY_AUTO_DEBUG should validate connection_id."""

    def test_validate_job_rejects_missing_connection_id(self):
        from worker.main import validate_job

        valid, msg = validate_job({
            "type": "SENTRY_AUTO_DEBUG",
            "error_id": "err-123",
            "user_id": "user-456",
        })
        assert not valid, "Should reject missing connection_id"
        assert "connection_id" in msg

    def test_validate_job_accepts_with_connection_id(self):
        from worker.main import validate_job

        valid, msg = validate_job({
            "type": "SENTRY_AUTO_DEBUG",
            "error_id": "err-123",
            "user_id": "user-456",
            "connection_id": "conn-789",
        })
        assert valid, f"Should accept valid job: {msg}"

    def test_validate_job_rejects_non_string_connection_id(self):
        from worker.main import validate_job

        valid, msg = validate_job({
            "type": "SENTRY_AUTO_DEBUG",
            "error_id": "err-123",
            "user_id": "user-456",
            "connection_id": 123,
        })
        assert not valid
        assert "string" in msg


class TestV206CompactFunctionsExist:
    """V-206: /compact should reference functions that actually exist."""

    def test_auto_compact_history_exists(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        assert "def _auto_compact_history(" in src, "_auto_compact_history should be defined"

    def test_force_compact_history_exists(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        assert "def _force_compact_history(" in src, "_force_compact_history should be defined"

    def test_compact_command_calls_both(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find('"/compact"')
        assert idx != -1, "/compact command should exist"
        block = src[idx:idx + 300]
        assert "_auto_compact_history" in block
        assert "_force_compact_history" in block


class TestV212BulkUnquarantineAllSettled:
    """V-212: handleBulkUnquarantine should use Promise.allSettled."""

    def test_uses_all_settled(self):
        src = pathlib.Path("web/src/components/dashboard/FlakyContent.tsx").read_text()
        idx = src.find("handleBulkUnquarantine")
        assert idx != -1
        block = src[idx:idx + 500]
        assert "allSettled" in block, "Should use Promise.allSettled, not Promise.all"
        assert "failures" in block, "Should track individual failures"


class TestV215HighlightMarkerA11y:
    """V-215: SessionPlayer highlight markers should have aria-label and keyboard access."""

    def test_highlight_markers_are_buttons(self):
        src = pathlib.Path("web/src/components/SessionPlayer.tsx").read_text()
        idx = src.find("Highlight markers")
        assert idx != -1
        block = src[idx:idx + 800]
        assert "<button" in block, "Markers should be <button> elements for keyboard access"
        assert 'aria-label' in block, "Markers should have aria-label"


class TestV216SelectAllCheckboxA11y:
    """V-216: FlakyContent select-all checkbox should have aria-label."""

    def test_select_all_has_aria_label(self):
        src = pathlib.Path("web/src/components/dashboard/FlakyContent.tsx").read_text()
        # Find the select-all checkbox (it checks selectedQuarantined.size)
        idx = src.find("Select all quarantined")
        assert idx != -1, "select-all checkbox should have aria-label"


class TestV221UseInsightsErrorState:
    """V-221: useInsights should expose error state, not swallow errors."""

    def test_returns_error_field(self):
        src = pathlib.Path("web/src/hooks/useInsights.ts").read_text()
        assert "error" in src, "Hook should expose error state"
        assert "setError" in src, "Hook should have setError"
        # Should return error in the return object
        ret_idx = src.find("return {")
        assert ret_idx != -1
        ret_block = src[ret_idx:ret_idx + 100]
        assert "error" in ret_block, "Return object should include error"


class TestV164RootCauseGuard:
    """V-164: RootCauseExplanation(**root_cause) should not crash on schema mismatch."""

    def test_has_try_except_guard(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("RootCauseExplanation(**root_cause)")
        assert idx != -1
        # Check that there's a try/except around it
        block = src[max(0, idx - 200):idx + 200]
        assert "try:" in block, "Should have try/except guard around RootCauseExplanation construction"
        assert "except" in block


class TestV165FlakyAndFailedClamp:
    """V-165: flaky_and_failed should be clamped to valid range."""

    def test_clamp_logic_exists(self):
        src = pathlib.Path("api/ai/flaky_predictor.py").read_text()
        # Find the clamp line specifically (not the comment or int() line)
        idx = src.find("Clamp to valid range")
        assert idx != -1, "Should have clamp comment"
        block = src[idx:idx + 200]
        assert "min(" in block, "Should clamp flaky_and_failed with min()"

    def test_clamp_prevents_negative_failures(self):
        """flaky_and_failed > failed should not make non_flaky_failures negative."""
        src = pathlib.Path("api/ai/flaky_predictor.py").read_text()
        idx = src.find("Clamp to valid range")
        assert idx != -1
        block = src[idx:idx + 200]
        assert "flaky_and_failed" in block and "min(" in block


class TestV187CreditCardPrefixValidation:
    """V-187: Credit card regex should validate card-specific prefixes."""

    def test_regex_matches_visa(self):
        from worker.privacy import PII_PATTERNS

        cc_re = PII_PATTERNS["credit_card"]
        assert cc_re.search("4111 1111 1111 1111"), "Should match Visa (starts with 4)"

    def test_regex_matches_mastercard(self):
        from worker.privacy import PII_PATTERNS

        cc_re = PII_PATTERNS["credit_card"]
        assert cc_re.search("5111 1111 1111 1111"), "Should match Mastercard (starts with 51-55)"

    def test_regex_matches_amex(self):
        from worker.privacy import PII_PATTERNS

        cc_re = PII_PATTERNS["credit_card"]
        # Amex: 15 digits, starts with 34 or 37, format 3xxx-xxxxxx-xxxxx
        assert cc_re.search("3711-1111-1111-111"), "Should match Amex (starts with 37)"

    def test_rejects_random_digit_sequences(self):
        from worker.privacy import PII_PATTERNS

        cc_re = PII_PATTERNS["credit_card"]
        assert not cc_re.search("1234567890123"), "Should not match arbitrary 13-digit number"
        assert not cc_re.search("9999 9999 9999 9999"), "Should not match non-card prefix"


# ─── Part 9: R9 Auth/Storage/Config scan fixes (V-226, V-228, V-230) ───


class TestV226StoragePathTraversal:
    """V-226: Storage path should reject traversal sequences."""

    def test_validate_storage_path_exists(self):
        src = pathlib.Path("api/storage.py").read_text()
        assert "_validate_storage_path" in src, "Should have path validation function"

    def test_rejects_dotdot(self):
        from api.storage import _validate_storage_path

        with pytest.raises(ValueError, match="Invalid storage path"):
            _validate_storage_path("../../secret")

    def test_rejects_leading_slash(self):
        from api.storage import _validate_storage_path

        with pytest.raises(ValueError, match="Invalid storage path"):
            _validate_storage_path("/etc/passwd")

    def test_accepts_normal_path(self):
        from api.storage import _validate_storage_path

        _validate_storage_path("run-123/report.json")  # No exception

    def test_upload_url_calls_validation(self):
        src = pathlib.Path("api/storage.py").read_text()
        idx = src.find("def sign_upload_url")
        block = src[idx:idx + 400]
        assert "_validate_storage_path" in block

    def test_download_url_calls_validation(self):
        src = pathlib.Path("api/storage.py").read_text()
        idx = src.find("def sign_download_url")
        block = src[idx:idx + 400]
        assert "_validate_storage_path" in block


class TestV228LockFilePermissions:
    """V-228: Credential lock file should be created with 0o600 permissions."""

    def test_lock_file_permissions_set(self):
        src = pathlib.Path("autodebug/config.py").read_text()
        idx = src.find(".credentials.lock")
        assert idx != -1
        block = src[idx:idx + 200]
        assert "0o600" in block, "Lock file should be created with 0o600 permissions"


class TestV230PiiScrubMinLength:
    """V-230: PII log scrubbing should catch tokens as short as 10 chars."""

    def test_token_min_length(self):
        from api.logging_config import scrub_pii

        # Short token (12 chars after prefix) should be scrubbed
        assert "[TOKEN]" in scrub_pii("sk-abcdef123456")
        # Long token should still be scrubbed
        assert "[TOKEN]" in scrub_pii("sk-abcdefghijklmnopqrstuvwxyz123456")

    def test_pattern_minimum_chars(self):
        src = pathlib.Path("api/logging_config.py").read_text()
        idx = src.find("TOKEN")
        assert idx != -1
        block = src[max(0, idx - 100):idx]
        # Should use {10,} not {20,}
        assert "{10," in block, "Token pattern should match 10+ chars after prefix"


# ─── Part 10: V-180, V-186, V-209, V-224 ───


class TestV180JWTPaddingConsistency:
    """V-180: JWT base64url padding should be consistent between login and whoami."""

    def test_login_uses_conditional_padding(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # Find the JWT validation in login
        idx = src.find("JWT base64url needs padding")
        assert idx != -1
        block = src[idx:idx + 300]
        # Should check padding != 4 before adding
        assert "padding != 4" in block or "padding == 4" in block, \
            "Login should conditionally pad (not always add 4 chars)"
        # Should NOT have the old unconditional pattern
        assert '+ "=" * (4 - len(' not in block, \
            "Should not use unconditional padding formula"

    def test_whoami_uses_conditional_padding(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def whoami")
        assert idx != -1
        block = src[idx:idx + 1500]
        assert "padding != 4" in block, "Whoami should conditionally pad"


class TestV186BrokerNoneWarning:
    """V-186: get_broker() should warn when broker is unavailable."""

    def test_logs_warning_on_none(self):
        src = pathlib.Path("worker/broker.py").read_text()
        idx = src.find("def get_broker")
        assert idx != -1
        block = src[idx:idx + 400]
        assert "warning" in block.lower(), "Should log warning when broker is None"
        assert "unavailable" in block.lower(), "Warning should mention unavailability"


class TestV209FlakyContentAbortController:
    """V-209: FlakyContent concurrent quarantine fetches should have AbortController."""

    def test_abort_controller_in_fetch(self):
        src = pathlib.Path("web/src/components/dashboard/FlakyContent.tsx").read_text()
        idx = src.find("fetchQuarantineData")
        assert idx != -1
        block = src[idx:idx + 1200]
        assert "AbortController" in block, "Should create AbortController"
        assert "signal" in block, "Should pass signal to fetch"
        assert "AbortError" in block, "Should handle AbortError"

    def test_cleanup_on_unmount(self):
        src = pathlib.Path("web/src/components/dashboard/FlakyContent.tsx").read_text()
        # Find the useEffect that calls fetchQuarantineData
        idx = src.find("quarantineAbortRef")
        assert idx != -1, "Should have quarantineAbortRef"
        # Check abort is called in cleanup
        assert "abort()" in src, "Should call abort() in cleanup"


class TestV224DevAuthRequestRequired:
    """V-224: Dev auth should require request context for IP verification."""

    def test_no_allow_no_request_bypass(self):
        src = pathlib.Path("api/auth.py").read_text()
        idx = src.find("verify_authorization")
        assert idx != -1
        block = src[idx:idx + 600]
        # Should NOT have the escape hatch
        assert "_dev_auth_allow_no_request" not in block, \
            "Should not have allow-no-request bypass in auth flow"

    def test_request_none_raises(self):
        src = pathlib.Path("api/auth.py").read_text()
        # Find the verify_authorization function's request None check
        idx = src.find("def verify_authorization")
        assert idx != -1
        block = src[idx:idx + 800]
        assert "request is None" in block, "Should check for None request"
        # After the None check, should raise (not return)
        none_idx = block.find("request is None")
        after_none = block[none_idx:none_idx + 200]
        assert "raise" in after_none, "Should raise ValueError when request is None"


# ─── Part 11: V-194, V-217, V-218, V-227 ───


class TestV194SmokeTestEnvGuard:
    """V-194: Smoke-test auth bypass should be gated by env var, not hardcoded string."""

    def test_no_hardcoded_phase2_string(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # The old hardcoded string should be gone
        assert "scan the codebase for any remaining Phase 2 TODOs" not in src, \
            "Hardcoded smoke-test string should be removed"

    def test_uses_env_var_guard(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("_is_smoke")
        assert idx != -1
        block = src[idx:idx + 200]
        assert "HIKAFLOW_SMOKE_TEST" in block, "Should use env var for smoke test guard"


class TestV217ReplayChatRunIdValidation:
    """V-217: ReplayChat should validate run IDs before navigation."""

    def test_validates_run_id_format(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("handleRunClick")
        assert idx != -1
        block = src[idx:idx + 300]
        assert "test(runId)" in block, "Should validate run ID format with regex"


class TestV218LearningInsightsMemo:
    """V-218: LearningInsights chart components should be memoized."""

    def test_acceptance_chart_memoized(self):
        src = pathlib.Path("web/src/components/LearningInsights.tsx").read_text()
        assert "memo(function AcceptanceChart" in src, "AcceptanceChart should be wrapped in memo"

    def test_top_exceptions_memoized(self):
        src = pathlib.Path("web/src/components/LearningInsights.tsx").read_text()
        assert "memo(function TopExceptions" in src, "TopExceptions should be wrapped in memo"


class TestV227SignedUrlValidation:
    """V-227: Signed URL response from Supabase should be validated."""

    def test_resolve_signed_url_exists(self):
        from api.storage import _resolve_signed_url
        assert callable(_resolve_signed_url)

    def test_rejects_foreign_host(self):
        from api.storage import _resolve_signed_url
        import os
        os.environ.setdefault("SUPABASE_URL", "https://example.supabase.co")
        with pytest.raises(ValueError, match="does not match"):
            _resolve_signed_url("https://evil.com/steal-data")

    def test_accepts_relative_path(self):
        from api.storage import _resolve_signed_url
        import os
        os.environ.setdefault("SUPABASE_URL", "https://example.supabase.co")
        result = _resolve_signed_url("/storage/v1/signed/abc")
        assert "example.supabase.co" in result

    def test_rejects_unexpected_format(self):
        from api.storage import _resolve_signed_url
        import os
        os.environ.setdefault("SUPABASE_URL", "https://example.supabase.co")
        with pytest.raises(ValueError, match="Unexpected"):
            _resolve_signed_url("ftp://evil.com/data")


# ── Part 12: V-189, V-192, V-210, V-211, V-170 ─────────────────────────────

class TestV189ZstdBombProtection:
    """V-189: read_jsonl_zstd must have a decompression size limit."""

    def test_max_output_size_constant_exists(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        assert "_MAX_DECOMPRESSED_SIZE" in src

    def test_max_output_size_value(self):
        from worker.session_processing import _MAX_DECOMPRESSED_SIZE
        # Should be 256 MB
        assert _MAX_DECOMPRESSED_SIZE == 256 * 1024 * 1024

    def test_decompress_passes_max_output_size(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        idx = src.find("def read_jsonl_zstd")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "max_output_size" in block

    def test_normal_decompress_works(self):
        import zstandard as zstd
        from worker.session_processing import read_jsonl_zstd
        import tempfile

        data = b'{"key": "value"}\n{"key2": "value2"}\n'
        cctx = zstd.ZstdCompressor()
        compressed = cctx.compress(data)

        with tempfile.NamedTemporaryFile(suffix=".jsonl.zst", delete=False) as f:
            f.write(compressed)
            f.flush()
            results = list(read_jsonl_zstd(pathlib.Path(f.name)))

        assert len(results) == 2
        assert results[0]["key"] == "value"


class TestV192ProjectPagination:
    """V-192: _find_project_by_repo should paginate instead of fetching 200."""

    def test_uses_pagination_loop(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _find_project_by_repo")
        assert idx >= 0
        block = src[idx:idx + 600]
        # Should have a loop with offset
        assert "for page in range" in block or "while" in block

    def test_no_hardcoded_200(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _find_project_by_repo")
        assert idx >= 0
        block = src[idx:idx + 600]
        assert "limit=200" not in block

    def test_has_max_pages_safety(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _find_project_by_repo")
        assert idx >= 0
        block = src[idx:idx + 600]
        assert "max_pages" in block

    def test_early_exit_on_short_page(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _find_project_by_repo")
        assert idx >= 0
        block = src[idx:idx + 900]
        assert "len(projects) < page_size" in block or "len(projects) < limit" in block


class TestV210SettingsStaleRef:
    """V-210: SettingsContent notification updates should not use stale ref."""

    def test_no_notifications_ref(self):
        src = pathlib.Path("web/src/components/dashboard/SettingsContent.tsx").read_text()
        assert "notificationsRef" not in src

    def test_useCallback_has_notifications_dep(self):
        src = pathlib.Path("web/src/components/dashboard/SettingsContent.tsx").read_text()
        idx = src.find("updateNotification")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "[notifications]" in block

    def test_no_unused_useRef_import(self):
        src = pathlib.Path("web/src/components/dashboard/SettingsContent.tsx").read_text()
        # useRef should not be imported if not used
        if "useRef" in src.split("\n")[0:10].__repr__():
            # If useRef is imported, it must be used somewhere
            assert "useRef(" in src


class TestV211ReplayChatAbortController:
    """V-211: ReplayChat should use AbortController for chat fetches."""

    def test_has_abort_controller_ref(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        assert "chatAbortRef" in src

    def test_abort_previous_on_new_send(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("const sendMessage")
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "chatAbortRef.current?.abort()" in block

    def test_signal_passed_to_fetch(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("const sendMessage")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "signal: ac.signal" in block or "signal:" in block

    def test_abort_error_handled(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("const sendMessage")
        assert idx >= 0
        block = src[idx:idx + 2000]
        assert "AbortError" in block

    def test_cleanup_on_unmount(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        assert "chatAbortRef.current?.abort()" in src


class TestV170LinearNotificationNoOp:
    """V-170: Linear notification should actually create an issue, not silently no-op."""

    def test_linear_block_calls_create_issue(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("if config.linear_team_id:")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "create_issue" in block or "add_comment" in block

    def test_no_placeholder_comment(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("if config.linear_team_id:")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "placeholder" not in block.lower()

    def test_issue_title_includes_test_name(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("if config.linear_team_id:")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "test_name" in block


# ── Part 13: V-213, V-214, V-195 ────────────────────────────────────────────

class TestV213DashboardTabsHydration:
    """V-213: DashboardTabs should use useEffect to sync URL params (no hydration mismatch)."""

    def test_uses_state_for_active_tab(self):
        src = pathlib.Path("web/src/components/DashboardTabs.tsx").read_text()
        assert "useState" in src
        assert "setActiveTab" in src

    def test_syncs_url_in_effect(self):
        src = pathlib.Path("web/src/components/DashboardTabs.tsx").read_text()
        assert "useEffect" in src
        # Find useEffect inside the component body (not in imports)
        idx = src.find("useEffect(")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "searchParams.get" in block or "tabFromUrl" in block

    def test_no_direct_searchparams_as_active(self):
        src = pathlib.Path("web/src/components/DashboardTabs.tsx").read_text()
        # Should NOT have: const activeTab = searchParams.get(...)
        assert 'const activeTab = searchParams.get' not in src


class TestV214PerOperationAbort:
    """V-214: useRunActions should have separate AbortController refs per operation."""

    def test_no_shared_abort_ref(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        assert "abortControllerRef" not in src

    def test_has_per_operation_refs(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        assert "selfHealAbortRef" in src
        assert "generateTestAbortRef" in src
        assert "reproAbortRef" in src
        assert "autoFixAbortRef" in src
        assert "pbtAbortRef" in src

    def test_cleanup_aborts_all(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        # Cleanup should abort all 5 refs
        idx = src.find("return () =>")
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "selfHealAbortRef.current?.abort()" in block
        assert "pbtAbortRef.current?.abort()" in block

    def test_each_operation_uses_own_ref(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        # triggerSelfHeal uses selfHealAbortRef
        idx = src.find("triggerSelfHeal")
        block = src[idx:idx + 200]
        assert "selfHealAbortRef" in block
        # triggerGenerateTest uses generateTestAbortRef
        idx = src.find("triggerGenerateTest")
        block = src[idx:idx + 200]
        assert "generateTestAbortRef" in block


class TestV195NonBlockingTelemetry:
    """V-195: _report_turn_outcome_to_api should not block the event loop."""

    def test_uses_background_thread(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _report_turn_outcome_to_api")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "threading" in block or "to_thread" in block or "Thread" in block

    def test_no_direct_requests_post_in_main_thread(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _report_turn_outcome_to_api")
        assert idx >= 0
        block = src[idx:idx + 1200]
        # requests.post should be inside a nested function, not at top level
        assert "def _post" in block

    def test_daemon_thread(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def _report_turn_outcome_to_api")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "daemon=True" in block


# ── Part 14: V-219, V-222, V-196 ────────────────────────────────────────────

class TestV219MessageOrdering:
    """V-219: ReplayChat should have sequence tracking to prevent stale responses."""

    def test_has_sequence_ref(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        assert "messageSeqRef" in src

    def test_sequence_increments_on_send(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("const sendMessage")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "++messageSeqRef.current" in block

    def test_stale_response_guard(self):
        src = pathlib.Path("web/src/components/ReplayChat.tsx").read_text()
        idx = src.find("const sendMessage")
        assert idx >= 0
        block = src[idx:idx + 2500]
        assert "messageSeqRef.current !== seq" in block


class TestV222JsonCatchFallbacks:
    """V-222: .json() calls should have .catch() fallbacks to prevent unhandled rejections."""

    def test_sentry_callback_has_catch(self):
        src = pathlib.Path("web/src/app/sentry/callback/page.tsx").read_text()
        # Both success and error branches should have .catch
        assert src.count(".json().catch") >= 2

    def test_self_healing_events_has_catch(self):
        src = pathlib.Path("web/src/components/SelfHealingEventsList.tsx").read_text()
        assert ".json().catch" in src

    def test_self_healing_config_has_catch(self):
        src = pathlib.Path("web/src/components/SelfHealingConfig.tsx").read_text()
        assert src.count(".json().catch") >= 2

    def test_self_healing_stats_has_catch(self):
        src = pathlib.Path("web/src/components/SelfHealingStats.tsx").read_text()
        assert ".json().catch" in src

    def test_pricing_page_has_catch(self):
        src = pathlib.Path("web/src/app/pricing/page.tsx").read_text()
        assert src.count(".json().catch") >= 2

    def test_run_diff_has_catch(self):
        src = pathlib.Path("web/src/components/RunDiff.tsx").read_text()
        assert src.count(".json().catch") >= 2


class TestV196TyperExitAsync:
    """V-196: typer.Exit(1) should not be raised inside async _run_debug_session."""

    def test_async_debug_returns_false_not_exit(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("async def _run_debug_session")
        assert idx >= 0
        block = src[idx:idx + 2000]
        # Should NOT have typer.Exit inside the async function
        assert "raise typer.Exit" not in block

    def test_sync_caller_handles_false(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # The sync caller should check for False and raise typer.Exit
        idx = src.find("_session_result = asyncio.run")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "_session_result is False" in block


# ── Part 15: V-197, V-220, V-223 ────────────────────────────────────────────

class TestV223TimelineKeyScope:
    """V-223: TimelineViewer '/' key should only fire when container has focus."""

    def test_slash_key_checks_active_element(self):
        src = pathlib.Path("web/src/components/timeline/TimelineViewer.tsx").read_text()
        idx = src.find('case "/"')
        assert idx >= 0
        block = src[idx:idx + 200]
        assert "document.activeElement" in block or "e.target" in block

    def test_slash_key_handler_exists(self):
        src = pathlib.Path("web/src/components/timeline/TimelineViewer.tsx").read_text()
        assert 'case "/"' in src
        assert "setSearchOpen(true)" in src


class TestV197AsyncioRunDefensive:
    """V-197: asyncio.run() in worker should handle running event loop."""

    def test_retention_purge_has_loop_check(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("purge_sessions")
        assert idx >= 0
        block = src[idx - 200:idx + 400]
        assert "get_running_loop" in block

    def test_highlight_summarize_has_loop_check(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("summarize_highlights")
        assert idx >= 0
        block = src[idx - 200:idx + 400]
        assert "get_running_loop" in block

    def test_threadpool_fallback(self):
        src = pathlib.Path("worker/main.py").read_text()
        assert "ThreadPoolExecutor" in src


class TestV220StaleRunPropAudit:
    """V-220: useRunActions callbacks should include run in dependency arrays (audit)."""

    def test_trigger_self_heal_deps_include_run(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        idx = src.find("triggerSelfHeal")
        assert idx >= 0
        # Find the closing dependency array for this callback
        block = src[idx:idx + 3000]
        # The useCallback for triggerSelfHeal should have [run, ...] deps
        dep_idx = block.find("], [run")
        assert dep_idx >= 0 or "run, setRun" in block

    def test_trigger_generate_test_deps_include_run(self):
        src = pathlib.Path("web/src/hooks/useRunActions.ts").read_text()
        idx = src.find("triggerGenerateTest")
        assert idx >= 0
        block = src[idx:idx + 1500]
        assert "[run]" in block


# ── Part 16: Batch 5 — Auth, Storage, Config ────────────────────────────────


class TestV225JWKSCacheSize:
    """V-225: JWKS cache should support 64+ issuers to avoid thrashing."""

    def test_max_issuers_at_least_64(self):
        src = pathlib.Path("api/auth.py").read_text()
        idx = src.find("max_issuers")
        assert idx >= 0
        block = src[idx:idx + 50]
        # Extract the integer value after "max_issuers"
        import re
        m = re.search(r"max_issuers.*?(\d+)", block)
        assert m is not None
        assert int(m.group(1)) >= 64

    def test_eviction_logging_present(self):
        src = pathlib.Path("api/auth.py").read_text()
        idx = src.find("max_issuers")
        assert idx >= 0
        # Look for eviction logging in the cache class (eviction is in _fetch_and_cache)
        block = src[idx:idx + 3000]
        assert "evict" in block.lower() or "JWKS cache full" in block


class TestV234BucketValidationLogging:
    """V-234: Storage functions should log rejected bucket names."""

    def test_sign_upload_url_logs_rejected_bucket(self):
        src = pathlib.Path("api/storage.py").read_text()
        idx = src.find("def sign_upload_url")
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "logger.warning" in block
        assert "bucket" in block.lower()

    def test_sign_download_url_logs_rejected_bucket(self):
        src = pathlib.Path("api/storage.py").read_text()
        idx = src.find("def sign_download_url")
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "logger.warning" in block
        assert "bucket" in block.lower()

    def test_download_text_logs_rejected_bucket(self):
        src = pathlib.Path("api/storage.py").read_text()
        idx = src.find("def download_text")
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "logger.warning" in block
        assert "bucket" in block.lower()


class TestV235ClerkAudienceWarning:
    """V-235: Production warning when CLERK_AUDIENCE is not set."""

    def test_clerk_audience_warning_exists(self):
        src = pathlib.Path("api/auth.py").read_text()
        assert "CLERK_AUDIENCE" in src
        # Warning should mention audience verification being disabled
        assert "Audience verification" in src or "audience verification" in src or "aud verification" in src

    def test_clerk_audience_env_check_at_startup(self):
        src = pathlib.Path("api/auth.py").read_text()
        # Should check for CLERK_AUDIENCE being unset
        idx = src.find("CLERK_AUDIENCE is not set")
        assert idx >= 0

    def test_clerk_audience_production_guard(self):
        src = pathlib.Path("api/auth.py").read_text()
        # The warning should only fire in production
        idx = src.find("CLERK_AUDIENCE is not set")
        assert idx >= 0
        # Look backwards for production check
        block = src[max(0, idx - 300):idx]
        assert "production" in block.lower() or "prod" in block.lower()


class TestV233JWKSSingleflightTimeout:
    """V-233: JWKS singleflight timeout — verify it's >= HTTP timeout."""

    def test_singleflight_timeout_gte_http_timeout(self):
        src = pathlib.Path("api/auth.py").read_text()
        import re
        # Find the event.wait timeout
        m_wait = re.search(r"event\.wait\(timeout=(\d+)\)", src)
        assert m_wait is not None
        wait_timeout = int(m_wait.group(1))
        # Find the HTTP GET timeout
        m_http = re.search(r"session\.get\(url,\s*timeout=(\d+)\)", src)
        assert m_http is not None
        http_timeout = int(m_http.group(1))
        # Singleflight must be >= HTTP timeout to avoid premature fallthrough
        assert wait_timeout >= http_timeout

    def test_singleflight_fallthrough_on_timeout(self):
        src = pathlib.Path("api/auth.py").read_text()
        idx = src.find("event.wait(timeout=")
        assert idx >= 0
        block = src[idx:idx + 300]
        # After waiting, code should fall through to fetch ourselves
        assert "fall through" in block.lower() or "fetch" in block.lower()


# ── Part 17: Batch 6 — Misc tractable verdicts ──────────────────────────────


class TestV41MCPToolCount:
    """V-41: server_v2.py docstring should match actual tool count."""

    def test_docstring_tool_count_matches_decorators(self):
        src = pathlib.Path("mcp_server/server_v2.py").read_text()
        import re
        # Count @mcp.tool() decorators
        actual = len(re.findall(r"@mcp\.tool\(\)", src))
        # Extract the docstring claim
        m = re.search(r"Exposes (\d+) tools", src)
        assert m is not None
        claimed = int(m.group(1))
        assert claimed == actual, f"Docstring says {claimed} but found {actual}"


class TestV45FlakyRunsConstraint:
    """V-45: flaky measure --runs should have range constraints."""

    def test_runs_has_min_max(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # Find the "measure" command definition, then its --runs
        idx = src.find('"measure"')
        assert idx >= 0
        runs_idx = src.find('"--runs"', idx)
        assert runs_idx >= 0
        block = src[runs_idx:runs_idx + 200]
        assert "min=" in block or "min =" in block
        assert "max=" in block or "max =" in block


class TestV78DLQTrimBeforeLen:
    """V-78: DLQ should ltrim before llen for accurate count."""

    def test_ltrim_before_llen_in_pipeline(self):
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("pipe.lpush(DEAD_LETTER_QUEUE")
        assert idx >= 0
        block = src[idx:idx + 300]
        # ltrim should appear before llen in the pipeline sequence
        ltrim_pos = block.find("ltrim")
        llen_pos = block.find("llen")
        assert ltrim_pos >= 0 and llen_pos >= 0
        assert ltrim_pos < llen_pos, "ltrim should come before llen in pipeline"

    def test_dlq_len_reads_post_trim_index(self):
        src = pathlib.Path("worker/queue.py").read_text()
        # After reorder: lpush[0], ltrim[1], llen[2]
        idx = src.find("dlq_len = results[")
        assert idx >= 0
        block = src[idx:idx + 30]
        assert "results[2]" in block


class TestV130AIReasoningPanelDeps:
    """V-130: AIReasoningPanel should use scalar deps, not object refs."""

    def test_no_raw_object_deps(self):
        src = pathlib.Path("web/src/components/ai-reasoning/AIReasoningPanel.tsx").read_text()
        idx = src.find("useEffect(")
        assert idx >= 0
        block = src[idx:idx + 800]
        import re
        # Find the dependency array (may be "}, [" or "], [")
        m = re.search(r"[}\]],\s*\[([^\]]+)\]", block)
        assert m is not None, "Could not find dependency array"
        deps = m.group(1)
        # Should reference scalar properties, not raw objects
        assert "debate.analysis" in deps or "debate.loading" in deps
        assert "hypotheses.data" in deps or "hypotheses.loading" in deps


class TestV131SessionPlayerMountedGuard:
    """V-131: SessionPlayer async init should check isMounted before setState."""

    def test_is_mounted_flag_exists(self):
        src = pathlib.Path("web/src/components/SessionPlayer.tsx").read_text()
        idx = src.find("initReplayer")
        assert idx >= 0
        block = src[idx:idx + 1500]
        assert "isMounted" in block

    def test_mounted_check_before_set_state(self):
        src = pathlib.Path("web/src/components/SessionPlayer.tsx").read_text()
        idx = src.find("initReplayer")
        assert idx >= 0
        block = src[idx:idx + 1500]
        # isMounted check should appear before setDuration
        mount_check = block.find("if (!isMounted)")
        set_duration = block.find("setDuration")
        assert mount_check >= 0 and set_duration >= 0
        assert mount_check < set_duration

    def test_cleanup_sets_mounted_false(self):
        src = pathlib.Path("web/src/components/SessionPlayer.tsx").read_text()
        idx = src.find("initReplayer")
        assert idx >= 0
        block = src[idx:idx + 2000]
        # Cleanup should set isMounted = false
        assert "isMounted = false" in block


class TestV149BanditThreadSafety:
    """V-149: ThompsonSamplingSelector should use threading.Lock."""

    def test_lock_attribute_exists(self):
        src = pathlib.Path("autodebug/strategies/bandit_selector.py").read_text()
        idx = src.find("class ThompsonSamplingSelector")
        assert idx >= 0
        block = src[idx:idx + 1500]
        assert "_lock" in block
        assert "threading" in src

    def test_select_strategies_uses_lock(self):
        src = pathlib.Path("autodebug/strategies/bandit_selector.py").read_text()
        idx = src.find("def select_strategies(")
        assert idx >= 0
        block = src[idx:idx + 1500]
        assert "self._lock" in block

    def test_update_uses_lock(self):
        src = pathlib.Path("autodebug/strategies/bandit_selector.py").read_text()
        idx = src.find("def update(")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "self._lock" in block


class TestV151QueueSuspendLock:
    """V-151: _queue_suspend_until_monotonic reads/writes should be locked."""

    def test_is_queue_suspended_uses_lock(self):
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("def _is_queue_suspended")
        assert idx >= 0
        block = src[idx:idx + 200]
        assert "_redis_lock" in block

    def test_handle_queue_pop_error_uses_lock(self):
        src = pathlib.Path("worker/queue.py").read_text()
        idx = src.find("def _handle_queue_pop_error")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "_redis_lock" in block
        # Lock should be around the suspend_until write
        assert "_queue_suspend_until_monotonic" in block


# ── Part 18: Batch 7 — Skills, CLI, API, frontend cleanup ───────────────────


class TestV42EditBlockReplace:
    """V-42: knowledge_memory edit_block should support mode='replace'."""

    def test_replace_mode_supported(self):
        src = pathlib.Path("autodebug/agent/tools.py").read_text()
        idx = src.find('action == "edit_block"')
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert '"replace"' in block

    def test_error_message_includes_replace(self):
        src = pathlib.Path("autodebug/agent/tools.py").read_text()
        idx = src.find('action == "edit_block"')
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "replace" in block


class TestV43InvestigateTriggers:
    """V-43: investigate skill should not have 'read' as trigger."""

    def test_no_bare_read_trigger(self):
        src = pathlib.Path("autodebug/agent/skills/__init__.py").read_text()
        idx = src.find('name="investigate"')
        assert idx >= 0
        block = src[idx:idx + 400]
        # Extract the triggers tuple content
        triggers_start = block.find("triggers=(")
        assert triggers_start >= 0
        triggers_block = block[triggers_start:triggers_start + 300]
        triggers_end = triggers_block.find(")")
        triggers_str = triggers_block[:triggers_end]
        # Should NOT contain bare "read" as a trigger
        assert '"read"' not in triggers_str


class TestV44StrategiesDocumented:
    """V-44: generate-tests --strategies help text documents all 6 values."""

    def test_all_strategies_in_help(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("--strategies")
        assert idx >= 0
        block = src[idx:idx + 300]
        for strategy in ["happy_path", "edge_cases", "error_cases", "null_checks", "type_variants", "integration"]:
            assert strategy in block, f"Missing strategy: {strategy}"


class TestV57V1StrategyListComplete:
    """V-57: v1 CLI should include 'integration' in strategy choices."""

    def test_v1_cli_has_integration(self):
        src = pathlib.Path("autodebug/cli_commands.py").read_text()
        idx = src.find("--strategies")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "integration" in block


class TestV77MetricsPermission:
    """V-77: /metrics endpoint should check permission scope."""

    def test_metrics_requires_permission(self):
        src = pathlib.Path("api/main.py").read_text()
        idx = src.find('"/metrics"')
        assert idx >= 0
        block = src[idx:idx + 400]
        assert "require_permission" in block or "permission" in block.lower()


class TestV132NoDoubleFiltering:
    """V-132: RunsContent should not double-filter runs client-side."""

    def test_no_client_side_filter(self):
        src = pathlib.Path("web/src/components/dashboard/RunsContent.tsx").read_text()
        # Should not have a filteredRuns variable
        assert "filteredRuns" not in src


class TestV150StreamingClose:
    """V-150: Streaming client should close OpenAI connection in finally."""

    def test_aclose_in_finally(self):
        src = pathlib.Path("api/routers/llm_proxy.py").read_text()
        idx = src.find("async for chunk")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "aclose" in block
        assert "finally" in block


class TestV153EnsureFutureCallback:
    """V-153: ensure_future for PR merge should have exception callback."""

    def test_done_callback_attached(self):
        src = pathlib.Path("api/github_app.py").read_text()
        idx = src.find("handle_pr_merged")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "add_done_callback" in block

    def test_exception_logged(self):
        src = pathlib.Path("api/github_app.py").read_text()
        idx = src.find("handle_pr_merged")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "exception()" in block or "logger.error" in block


# ── Part 19: Batch 8 — CLI flags, search, dedup, self-healer ────────────────


class TestV58WatchJsonOutput:
    """V-58: watch and generate-tests should have --json flag."""

    def test_watch_has_json_flag(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def watch(")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert '"--json"' in block

    def test_generate_tests_has_json_flag(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def generate_tests(")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert '"--json"' in block


class TestV59AutoAnalyzeDocumentation:
    """V-59: watch --auto-analyze should document AI key requirement."""

    def test_docstring_mentions_api_key(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def watch(")
        assert idx >= 0
        block = src[idx:idx + 1200]
        assert "API_KEY" in block or "api_key" in block.lower() or "Requires" in block


class TestV60WatchNotifyFlag:
    """V-60: watch should have --notify/--no-notify flag."""

    def test_notify_flag_exists(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("def watch(")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "--notify" in block


class TestV137SearchRunsOverfetch:
    """V-137: search_runs should fetch extra rows when client-side filters active."""

    def test_overfetch_when_filters_present(self):
        src = pathlib.Path("api/db.py").read_text()
        idx = src.find("def search_runs(")
        assert idx >= 0
        block = src[idx:idx + 2000]
        assert "fetch_limit" in block or "limit * 3" in block or "limit *" in block

    def test_results_trimmed_to_requested_limit(self):
        src = pathlib.Path("api/db.py").read_text()
        idx = src.find("def search_runs(")
        assert idx >= 0
        block = src[idx:idx + 3500]
        assert "[:limit]" in block


class TestV154WebhookDedupThreadSafe:
    """V-154: Webhook delivery dedup should use threading.Lock."""

    def test_dedup_lock_exists(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        assert "_dedup_lock" in src
        assert "threading" in src

    def test_dedup_check_uses_lock(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        idx = src.find("def _check_github_delivery_dedup")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "_dedup_lock" in block


class TestV158SelfHealerRunReuse:
    """V-158: _check_regression should accept pre-fetched run to avoid double DB call."""

    def test_check_regression_accepts_run_param(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("def _check_regression(")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "run:" in block or "run =" in block

    def test_caller_passes_run(self):
        src = pathlib.Path("api/ai/self_healer.py").read_text()
        idx = src.find("_check_regression(")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "run=" in block


# ── Part 20: Batch 9 — Feature flags, Docker safety, parallelism, dedup ─────


class TestV112FeatureFlagDefault:
    """V-112: _env_flag default should be False (fail-safe)."""

    def test_default_is_false(self):
        src = pathlib.Path("autodebug/agent/tools.py").read_text()
        idx = src.find("def _env_flag(")
        assert idx >= 0
        block = src[idx:idx + 100]
        assert "default: bool = False" in block


class TestV179DockerInstallSafety:
    """V-179: Docker install should not use curl|sh pattern."""

    def test_no_curl_pipe_sh(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        # Should NOT have curl piped to sh
        assert "curl -fsSL https://get.docker.com | sh" not in src

    def test_shows_manual_instructions(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("get-docker")
        assert idx >= 0 or "docker.com" in src.lower()


class TestV111EarlyFlakeParallel:
    """V-111: early_flake should check new tests in parallel."""

    def test_uses_asyncio_gather(self):
        src = pathlib.Path("autodebug/detection/early_flake.py").read_text()
        idx = src.find("check_test")
        assert idx >= 0
        block = src[max(0, idx - 200):idx + 200]
        assert "gather" in block


class TestV103TestableDedup:
    """V-103: testable_functions should deduplicate by qualified_name."""

    def test_dedup_logic_present(self):
        src = pathlib.Path("autodebug/autonomous/test_generator.py").read_text()
        idx = src.find("def testable_functions")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "seen" in block or "qualified_name" in block

    def test_no_duplicates_in_property(self):
        src = pathlib.Path("autodebug/autonomous/test_generator.py").read_text()
        idx = src.find("def testable_functions")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "qualified_name" in block


# ── Part 21: Batch 10 — PII sampling docs, HTTP issuer warning ──────────────


class TestV188PIISamplingDocumented:
    """V-188: PII sampling gap should be documented."""

    def test_sampling_comment_exists(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        idx = src.find("PII sampling")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "intentionally" in block.lower() or "10%" in block or "performance" in block.lower()


class TestV99HttpIssuerWarning:
    """V-99: http JWT issuer should log warning outside production."""

    def test_http_issuer_warns(self):
        src = pathlib.Path("api/auth.py").read_text()
        idx = src.find("_validate_issuer")
        assert idx >= 0
        block = src[idx:idx + 2000]
        assert "http" in block and "warning" in block.lower() or "logger.warning" in block


# ── Part 22: Batch 11 — Double timeout, port race, billing session, useRunDetail ─


class TestV161SingleTimeout:
    """V-161: Remove double-timeout (wait_for + session_timeout_s)."""

    def test_no_wait_for_wrapping_run_debug_session(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("_run_debug_session")
        assert idx >= 0
        # Look back 200 chars to check there's no wait_for wrapping it
        pre = src[max(0, idx - 200):idx]
        assert "wait_for" not in pre, "asyncio.wait_for should not wrap _run_debug_session"

    def test_session_timeout_s_still_passed(self):
        src = pathlib.Path("autodebug/cli_v2.py").read_text()
        idx = src.find("_run_debug_session")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "session_timeout_s=" in block


class TestV96OAuthPortRace:
    """V-96: OAuth port finder should use SO_REUSEADDR to mitigate TOCTOU race."""

    def test_so_reuseaddr_present(self):
        src = pathlib.Path("autodebug/oauth.py").read_text()
        idx = src.find("_find_available_port")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "SO_REUSEADDR" in block

    def test_setsockopt_before_bind(self):
        src = pathlib.Path("autodebug/oauth.py").read_text()
        idx = src.find("def _find_available_port")
        assert idx >= 0
        block = src[idx:idx + 800]
        setsockopt_pos = block.find("setsockopt")
        bind_pos = block.find("s.bind(")
        assert setsockopt_pos >= 0 and bind_pos >= 0
        assert setsockopt_pos < bind_pos, "setsockopt must come before bind"


class TestV139BillingConnectionPool:
    """V-139: billing.py should use a shared requests.Session, not bare requests."""

    def test_module_session_exists(self):
        src = pathlib.Path("api/db/billing.py").read_text()
        assert "_session = requests.Session()" in src

    def test_no_bare_requests_calls(self):
        import re
        src = pathlib.Path("api/db/billing.py").read_text()
        # Find bare requests.get/post/patch/head/put/delete calls (not _session.*)
        bare = re.findall(r"(?<!\w)requests\.(get|post|patch|head|put|delete)\(", src)
        assert len(bare) == 0, f"Found bare requests calls: {bare}"

    def test_session_used_in_functions(self):
        src = pathlib.Path("api/db/billing.py").read_text()
        assert src.count("_session.") >= 10, "Expected at least 10 _session.* calls"


class TestV127UseRunDetailShared:
    """V-127: useRunDetail should use shared fetch logic, not duplicate transcript fetch."""

    def test_shared_fetch_function_exists(self):
        src = pathlib.Path("web/src/hooks/useRunDetail.ts").read_text()
        assert "fetchRunWithTranscript" in src

    def test_refetch_uses_shared_function(self):
        src = pathlib.Path("web/src/hooks/useRunDetail.ts").read_text()
        idx = src.find("refetch")
        assert idx >= 0
        block = src[idx:idx + 300]
        assert "fetchRunWithTranscript" in block

    def test_useeffect_uses_shared_function(self):
        src = pathlib.Path("web/src/hooks/useRunDetail.ts").read_text()
        idx = src.find("useEffect(")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "fetchRunWithTranscript" in block

    def test_no_duplicate_transcript_in_refetch(self):
        src = pathlib.Path("web/src/hooks/useRunDetail.ts").read_text()
        # The transcript fetch pattern should appear only once (in fetchRunWithTranscript)
        count = src.count("/transcript")
        assert count == 1, f"Expected exactly 1 transcript fetch, found {count}"


# ── Part 23: Ship-blocker fixes ─────────────────────────────────────────────


class TestV183CleanupWorkDir:
    """V-183: _cleanup_work_dir must not delete the shared WORK_DIR root."""

    def test_no_shutil_rmtree_work_dir(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("def _cleanup_work_dir")
        assert idx >= 0
        block = src[idx:idx + 600]
        assert "shutil.rmtree(WORK_DIR)" not in block, \
            "_cleanup_work_dir must not rmtree the root WORK_DIR"

    def test_iterates_children(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("def _cleanup_work_dir")
        assert idx >= 0
        block = src[idx:idx + 600]
        assert ".iterdir()" in block or "os.listdir" in block

    def test_per_job_cleanup_exists_in_process_run(self):
        src = pathlib.Path("worker/main.py").read_text()
        idx = src.find("def process_run")
        assert idx >= 0
        block = src[idx:idx + 4000]
        assert "shutil.rmtree(dest)" in block, \
            "process_run should clean up its own temp dir"


class TestV75PIIRedaction:
    """V-75: Non-DOM events must have PII redacted before DB storage."""

    def test_redact_pii_imported(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        assert "redact_pii" in src

    def test_redact_event_function_exists(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        assert "def _redact_event" in src

    def test_redacted_event_stored(self):
        src = pathlib.Path("worker/session_processing.py").read_text()
        idx = src.find("Store non-DOM events")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "_redact_event" in block

    def test_redact_pii_function_works(self):
        from worker.privacy import redact_pii
        result = redact_pii("Contact me at user@example.com for details")
        assert "user@example.com" not in result
        assert "[PII_REDACTED]" in result


class TestV231HttpPoolTimeout:
    """V-231: HTTP client must have pool timeout to prevent pool exhaustion."""

    def test_pool_timeout_configured(self):
        src = pathlib.Path("api/http_client.py").read_text()
        assert "pool=" in src or "pool_timeout" in src

    def test_httpx_timeout_object_used(self):
        src = pathlib.Path("api/http_client.py").read_text()
        assert "httpx.Timeout" in src


class TestV98WebhookDedupPersistence:
    """V-98: Webhook dedup should use Redis when available."""

    def test_redis_dedup_function_exists(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        assert "def _redis_dedup_check" in src

    def test_dedup_tries_redis_first(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        idx = src.find("def _check_github_delivery_dedup")
        assert idx >= 0
        block = src[idx:idx + 500]
        assert "_redis_dedup_check" in block

    def test_fallback_to_memory(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        idx = src.find("def _check_github_delivery_dedup")
        assert idx >= 0
        block = src[idx:idx + 800]
        assert "_processed_deliveries" in block, "Must fall back to in-memory"

    def test_dedup_ttl_defined(self):
        src = pathlib.Path("api/routers/webhooks.py").read_text()
        assert "_DEDUP_TTL_SECONDS" in src


class TestReadmeNaming:
    """README must use Hikaflow branding, not AutoDebug."""

    def test_title_is_hikaflow(self):
        src = pathlib.Path("README.md").read_text()
        first_line = src.split("\n")[0]
        assert "Hikaflow" in first_line
        assert "AutoDebug" not in first_line

    def test_no_autodebug_in_intro(self):
        src = pathlib.Path("README.md").read_text()
        # Check first 500 chars for branding
        intro = src[:500]
        assert "AutoDebug" not in intro


class TestDocsContent:
    """Docs page must have real content, not placeholders."""

    def test_getting_started_has_code(self):
        src = pathlib.Path("web/src/app/docs/DocsContent.tsx").read_text()
        assert "hikaflow login" in src
        assert "hikaflow run" in src

    def test_ci_integration_section_exists(self):
        src = pathlib.Path("web/src/app/docs/DocsContent.tsx").read_text()
        assert "ci-integration" in src
        assert "GitHub Actions" in src

    def test_self_hosted_has_docker(self):
        src = pathlib.Path("web/src/app/docs/DocsContent.tsx").read_text()
        assert "docker" in src.lower()
        assert ".env.example" in src

    def test_api_section_has_endpoints(self):
        src = pathlib.Path("web/src/app/docs/DocsContent.tsx").read_text()
        assert "/v1/runs" in src
