"""Test quality gates - ensure the test suite itself is high quality.

These meta-tests prevent common testing anti-patterns and ensure
comprehensive coverage of source modules.
"""

from __future__ import annotations

import ast
import re
import subprocess
from pathlib import Path

import pytest


def _git_read(path: str) -> str:
    """Read file content via git to avoid filesystem timeouts."""
    try:
        result = subprocess.run(
            ["git", "show", f"HEAD:{path}"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return result.stdout
    except Exception:
        pass
    return ""


def _safe_read(path: Path) -> str:
    """Read file content safely, trying git first then filesystem."""
    cwd = Path.cwd()
    try:
        # Try to get relative path for git
        if path.is_absolute() and str(path).startswith(str(cwd)):
            rel_path = path.relative_to(cwd)
        else:
            rel_path = path
        content = _git_read(str(rel_path))
        if content:
            return content
    except ValueError:
        pass

    # Fallback to direct read
    try:
        return path.read_text()
    except Exception:
        return ""


def _collect_python_files(directory: str, pattern: str = "*.py") -> list[Path]:
    """Collect Python files from a directory."""
    base = Path(directory)
    if not base.exists():
        return []
    return list(base.rglob(pattern))


def _parse_ast_safe(content: str) -> ast.Module | None:
    """Parse Python code to AST, returning None on error."""
    try:
        return ast.parse(content)
    except SyntaxError:
        return None


class TestNoSleepInTests:
    """Tests should not use time.sleep() as it causes flakiness."""

    def test_no_sleep_calls(self):
        """No test file uses time.sleep()."""
        violations = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            # Check for time.sleep calls
            if "time.sleep" in content:
                # Parse to verify it's an actual call, not a comment
                tree = _parse_ast_safe(content)
                if tree:
                    for node in ast.walk(tree):
                        if isinstance(node, ast.Call):
                            if isinstance(node.func, ast.Attribute):
                                if node.func.attr == "sleep":
                                    violations.append(test_file.name)
                                    break

        # Warn but don't fail - these are tech debt to address
        if violations:
            pytest.warns(UserWarning, match="time.sleep")
            import warnings
            warnings.warn(f"Tests using time.sleep (causes flakiness): {violations}", UserWarning)
        # Only fail if there are more than 3 violations (regression)
        assert len(violations) <= 3, f"Too many tests using time.sleep: {violations}"


class TestNoBroadExceptInTests:
    """Tests should not catch broad exceptions as they hide failures."""

    def test_no_bare_except(self):
        """No test file uses bare 'except:' clause."""
        violations = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            tree = _parse_ast_safe(content)
            if tree:
                for node in ast.walk(tree):
                    if isinstance(node, ast.ExceptHandler):
                        # Bare except: has type=None
                        if node.type is None:
                            violations.append(test_file.name)
                            break

        # Allow small number of existing violations but prevent regression
        assert len(violations) <= 3, f"Too many tests with bare 'except:': {violations}"


class TestNoHardcodedSecrets:
    """Tests should not contain real API keys or secrets."""

    SECRET_PATTERNS = [
        (r"sk-[a-zA-Z0-9]{20,}", "OpenAI API key"),
        (r"AKIA[A-Z0-9]{16}", "AWS Access Key ID"),
        (r"ghp_[a-zA-Z0-9]{36}", "GitHub personal access token"),
        (r"glpat-[a-zA-Z0-9\-]{20,}", "GitLab personal access token"),
        (r"xox[baprs]-[a-zA-Z0-9\-]+", "Slack token"),
    ]

    def test_no_api_keys_in_tests(self):
        """No test file contains real API keys."""
        violations = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            for pattern, name in self.SECRET_PATTERNS:
                if re.search(pattern, content):
                    violations.append(f"{test_file.name}: {name}")

        # Some test files legitimately contain secret patterns for testing redaction/scrubbing
        allowed_files = {"test_logging_config.py", "test_redaction_core.py", "test_redaction.py", "test_scan_findings_fixes.py"}
        real_violations = [v for v in violations if not any(af in v for af in allowed_files)]
        assert real_violations == [], f"Tests with hardcoded secrets: {real_violations}"


class TestModuleCoverage:
    """Ensure all source modules have corresponding tests."""

    def test_detection_modules_have_tests(self):
        """Each detection module has at least one test file."""
        detection_dir = Path("autodebug/detection")
        if not detection_dir.exists():
            pytest.skip("autodebug/detection not found")

        detection_modules = [
            f.stem for f in detection_dir.glob("*.py")
            if not f.name.startswith("_") and f.name != "__init__.py"
        ]

        # Check tests directory for coverage
        test_files = _collect_python_files("tests")
        test_names = [f.stem for f in test_files if f.name.startswith("test_")]
        test_content = ""
        for tf in test_files:
            if tf.name.startswith("test_"):
                content = _safe_read(tf)
                if content:
                    test_content += content

        uncovered = []
        for module in detection_modules:
            # Check if module is imported or referenced in any test
            if module not in test_content:
                uncovered.append(module)

        # Allow some uncovered modules but warn
        if len(uncovered) > len(detection_modules) // 2:
            pytest.fail(f"More than half of detection modules lack tests: {uncovered}")

    def test_replay_modules_have_tests(self):
        """Each replay module has at least one test."""
        replay_dir = Path("autodebug_replay")
        if not replay_dir.exists():
            pytest.skip("autodebug_replay not found")

        replay_modules = [
            f.stem for f in replay_dir.glob("*.py")
            if not f.name.startswith("_") and f.name != "__init__.py"
        ]

        test_files = _collect_python_files("tests")
        test_content = ""
        for tf in test_files:
            if tf.name.startswith("test_"):
                content = _safe_read(tf)
                if content:
                    test_content += content

        uncovered = []
        for module in replay_modules:
            if module not in test_content:
                uncovered.append(module)

        if len(uncovered) > len(replay_modules) // 2:
            pytest.fail(f"More than half of replay modules lack tests: {uncovered}")


class TestProbeReplayPairing:
    """Every patch module in probe should have a replay counterpart."""

    def test_patch_replay_pairs_exist(self):
        """Every *_patch.py has a corresponding *_replay.py."""
        probe_dir = Path("autodebug_probe")
        replay_dir = Path("autodebug_replay")

        if not probe_dir.exists() or not replay_dir.exists():
            pytest.skip("autodebug_probe or autodebug_replay not found")

        patch_modules = [
            f.stem.replace("_patch", "")
            for f in probe_dir.glob("*_patch.py")
        ]

        replay_modules = [
            f.stem.replace("_replay", "")
            for f in replay_dir.glob("*_replay.py")
        ]

        # Check that each patch has a replay
        missing_replay = []
        for patch in patch_modules:
            if patch not in replay_modules:
                missing_replay.append(f"{patch}_patch.py -> {patch}_replay.py")

        # Some patches might not need replay (e.g., pure recording)
        # So we just warn if many are missing
        if len(missing_replay) > len(patch_modules) // 2:
            pytest.fail(f"Many patch modules lack replay counterparts: {missing_replay}")


class TestNoOrphanTestFiles:
    """Test files should import actual source modules."""

    def test_tests_import_source_modules(self):
        """Each test file imports at least one source module."""
        source_prefixes = (
            "api", "worker", "autodebug",
            "autodebug_probe", "autodebug_replay",
        )

        orphans = []
        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            # Check if file imports any source module
            imports_source = any(
                f"from {prefix}" in content or f"import {prefix}" in content
                for prefix in source_prefixes
            )

            if not imports_source:
                orphans.append(test_file.name)

        # Allow some utility test files and integration tests that use fixtures
        allowed_orphans = {
            "conftest.py", "test_meta.py", "test_quality_gates.py",
            # Integration tests use conftest fixtures that handle imports
            "test_db_integration.py", "test_storage_integration.py",
            "test_db_postgrest.py", "test_worker_queue.py",
            "test_redis_integration.py", "test_integration_endpoints.py",
            # API tests use client fixture from conftest
            "test_api_endpoints.py", "test_api_edge_cases.py",
            "test_ai_endpoints.py", "test_billing_endpoints.py",
            "test_error_endpoints.py", "test_github_endpoints.py",
            "test_notification_endpoints.py", "test_pattern_endpoints.py",
            "test_self_heal_endpoints.py", "test_shared_endpoints.py",
            # These use pytest fixtures and properties
            "test_properties.py", "test_issue_groups.py",
            # Script/infra tests and integration tests via fixtures
            "test_reliability_delta_script.py", "test_telemetry_endpoints.py",
            "test_eval_harness.py", "test_phase5_rollout_script.py",
        }
        actual_orphans = set(orphans) - allowed_orphans

        assert actual_orphans == set(), f"Test files without source imports: {actual_orphans}"


class TestNoSkippedTestsWithoutReason:
    """Skipped tests should have a reason."""

    def test_skip_has_reason(self):
        """All @pytest.mark.skip have a reason parameter."""
        violations = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            # Look for @pytest.mark.skip without reason
            # Valid: @pytest.mark.skip(reason="...")
            # Invalid: @pytest.mark.skip
            lines = content.split("\n")
            for i, line in enumerate(lines, 1):
                if "@pytest.mark.skip" in line and "reason=" not in line:
                    # Might be on next line, check
                    if i < len(lines) and "reason=" not in lines[i]:
                        violations.append(f"{test_file.name}:{i}")

        # Some bare skips might be intentional, so we just warn if there are many
        if len(violations) > 5:
            pytest.fail(f"Many skipped tests without reason: {violations[:10]}")


class TestNoTodoCommentsInTests:
    """TODO comments in tests should be tracked as issues."""

    def test_limited_todos(self):
        """Tests should not have excessive TODO comments."""
        todo_count = 0
        todo_files = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            todos = len(re.findall(r"#\s*TODO", content, re.IGNORECASE))
            if todos > 0:
                todo_count += todos
                todo_files.append(f"{test_file.name}:{todos}")

        # Allow some TODOs but not too many
        if todo_count > 20:
            pytest.fail(f"Excessive TODO comments in tests ({todo_count}): {todo_files[:10]}")


class TestAssertionsAreSpecific:
    """Tests should use specific assertions, not just 'assert True'."""

    def test_no_assert_true_false(self):
        """No tests use 'assert True' or 'assert False' (meaningless)."""
        violations = []

        for test_file in _collect_python_files("tests"):
            if "__pycache__" in str(test_file):
                continue
            if not test_file.name.startswith("test_"):
                continue

            content = _safe_read(test_file)
            if not content:
                continue

            lines = content.split("\n")
            for i, line in enumerate(lines, 1):
                stripped = line.strip()
                if stripped == "assert True" or stripped == "assert False":
                    violations.append(f"{test_file.name}:{i}")

        # Allow up to 15 existing violations (tech debt) but prevent regression
        assert len(violations) <= 15, f"Too many tests with meaningless assertions: {violations}"
