"""Self-tests for quality infrastructure.

These tests ensure the quality detectors themselves work correctly.
This prevents the quality suite from silently degrading.
"""

from __future__ import annotations

import ast
import re
from dataclasses import FrozenInstanceError
from pathlib import Path

import pytest

from tests.quality._baseline import (
    Violation,
    content_hash,
    load_baseline,
)
from tests.quality._helpers import (
    find_source_files,
    find_test_files,
    parse_file_safe,
    read_file_safe,
    relative_path,
)


class TestParseFileSafe:
    """Tests for safe file parsing."""

    def test_valid_python_parses(self, tmp_path: Path) -> None:
        """Verify valid Python code parses successfully."""
        file = tmp_path / "valid.py"
        file.write_text("def foo(): pass\n")

        tree, error = parse_file_safe(file)

        assert tree is not None
        assert error is None

    def test_syntax_error_returns_message(self, tmp_path: Path) -> None:
        """Verify syntax errors return descriptive message instead of raising."""
        file = tmp_path / "invalid.py"
        file.write_text("def foo(\n")

        tree, error = parse_file_safe(file)

        assert tree is None
        assert error is not None
        assert "SyntaxError" in error
        assert str(file) in error

    def test_unicode_error_returns_message(self, tmp_path: Path) -> None:
        """Verify unicode errors return message instead of raising."""
        file = tmp_path / "binary.py"
        file.write_bytes(b"\x80\x81\x82")

        tree, error = parse_file_safe(file)

        assert tree is None
        assert error is not None
        assert "UnicodeDecodeError" in error


class TestReadFileSafe:
    """Tests for safe file reading."""

    def test_valid_file_reads(self, tmp_path: Path) -> None:
        """Verify valid files read successfully."""
        file = tmp_path / "test.txt"
        file.write_text("hello world")

        content, error = read_file_safe(file)

        assert content == "hello world"
        assert error is None

    def test_unicode_error_returns_message(self, tmp_path: Path) -> None:
        """Verify unicode errors return message instead of raising."""
        file = tmp_path / "binary.txt"
        file.write_bytes(b"\x80\x81\x82")

        content, error = read_file_safe(file)

        assert content is None
        assert error is not None
        assert "UnicodeDecodeError" in error


class TestViolation:
    """Tests for Violation dataclass."""

    def test_stable_id_with_detail(self) -> None:
        """Verify stable ID includes all components when detail provided."""
        v = Violation(
            rule="thin_wrapper",
            relative_path="src/foo.py",
            identifier="my_func",
            detail="wrapped_call",
        )

        assert v.stable_id == "thin_wrapper|src/foo.py|my_func|wrapped_call"

    def test_stable_id_without_detail(self) -> None:
        """Verify stable ID works without detail."""
        v = Violation(
            rule="high_complexity",
            relative_path="src/bar.py",
            identifier="complex_func",
        )

        assert v.stable_id == "high_complexity|src/bar.py|complex_func"

    def test_str_representation(self) -> None:
        """Verify string representation is human-readable."""
        v = Violation(
            rule="test_rule",
            relative_path="src/module.py",
            identifier="function_name",
            detail="extra info",
        )

        result = str(v)

        assert "src/module.py" in result
        assert "function_name" in result
        assert "extra info" in result

    def test_frozen_immutable(self) -> None:
        """Verify Violation is immutable (frozen dataclass)."""
        v = Violation(
            rule="test",
            relative_path="test.py",
            identifier="func",
        )

        with pytest.raises(FrozenInstanceError):
            v.rule = "changed"  # type: ignore[misc]


class TestContentHash:
    """Tests for content hashing."""

    def test_same_content_same_hash(self) -> None:
        """Verify same content produces same hash."""
        content = "# TODO: fix this"

        assert content_hash(content) == content_hash(content)

    def test_different_content_different_hash(self) -> None:
        """Verify different content produces different hash."""
        assert content_hash("foo") != content_hash("bar")

    def test_hash_length_default(self) -> None:
        """Verify default hash length is 8."""
        result = content_hash("test")

        assert len(result) == 8

    def test_hash_length_custom(self) -> None:
        """Verify custom hash length works."""
        result = content_hash("test", length=16)

        assert len(result) == 16


class TestRelativePath:
    """Tests for relative path calculation."""

    def test_path_under_project_root(self) -> None:
        """Verify paths under project root are made relative."""
        from tests.quality._helpers import PROJECT_ROOT

        abs_path = PROJECT_ROOT / "src" / "noteflow" / "module.py"
        result = relative_path(abs_path)

        assert result == "src/noteflow/module.py"
        assert not result.startswith("/")

    def test_path_outside_project_root(self, tmp_path: Path) -> None:
        """Verify paths outside project root return absolute path."""
        abs_path = tmp_path / "external.py"
        result = relative_path(abs_path)

        assert str(tmp_path) in result


class TestFindSourceFiles:
    """Tests for source file discovery."""

    def test_finds_python_files(self) -> None:
        """Verify Python files are found."""
        files = find_source_files()

        assert len(files) > 0
        assert all(f.suffix == ".py" for f in files)

    def test_excludes_generated_files(self) -> None:
        """Verify generated protobuf files are excluded."""
        files = find_source_files()

        assert all("_pb2.py" not in str(f) for f in files)
        assert all("_pb2_grpc.py" not in str(f) for f in files)

    def test_excludes_venv(self) -> None:
        """Verify .venv directory is excluded."""
        files = find_source_files()

        assert all(".venv" not in f.parts for f in files)

    def test_excludes_tests_by_default(self) -> None:
        """Verify test files are excluded by default."""
        files = find_source_files()

        assert all("tests" not in f.parts for f in files)

    def test_includes_tests_when_requested(self) -> None:
        """Verify test files are included when searching from project root."""
        from tests.quality._helpers import PROJECT_ROOT

        # Must search from PROJECT_ROOT to find tests directory
        files = find_source_files(
            root=PROJECT_ROOT,
            include_tests=True,
            include_quality=True,
        )

        assert any("tests" in f.parts for f in files)

    def test_sorted_for_determinism(self) -> None:
        """Verify files are sorted for deterministic ordering."""
        files1 = find_source_files()
        files2 = find_source_files()

        assert files1 == files2


class TestFindTestFiles:
    """Tests for test file discovery."""

    def test_finds_test_files(self) -> None:
        """Verify test files are found."""
        files = find_test_files()

        assert len(files) > 0
        assert all(f.name.startswith("test_") for f in files)

    def test_excludes_quality_by_default(self) -> None:
        """Verify quality tests are excluded by default."""
        files = find_test_files()

        assert all("quality" not in f.parts for f in files)

    def test_includes_quality_when_requested(self) -> None:
        """Verify quality tests are included when requested."""
        files = find_test_files(include_quality=True)

        assert any("quality" in f.parts for f in files)


class TestBaselineLoading:
    """Tests for baseline loading."""

    def test_loads_existing_baseline(self) -> None:
        """Verify existing baseline loads without error."""
        baseline = load_baseline()

        assert isinstance(baseline, dict)

    def test_empty_rules_returns_empty_set(self) -> None:
        """Verify missing rule returns empty set."""
        baseline = load_baseline()

        nonexistent = baseline.get("nonexistent_rule", set())

        assert nonexistent == set()


# =============================================================================
# Detector Self-Tests: These validate that quality detectors work correctly
# =============================================================================


class TestSkipifDetection:
    """Self-tests for skipif detection (fixes detection hole)."""

    def test_detects_skip_without_reason(self) -> None:
        """Verify detection of @pytest.mark.skip without reason."""
        code = """
@pytest.mark.skip
def test_something():
    pass
"""
        skip_pattern = re.compile(
            r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE
        )

        matches = skip_pattern.findall(code)

        assert len(matches) == 1

    def test_detects_skip_with_empty_parens(self) -> None:
        """Verify detection of @pytest.mark.skip() with empty parens."""
        code = "@pytest.mark.skip()\ndef test_foo(): pass"
        skip_pattern = re.compile(
            r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE
        )

        assert skip_pattern.search(code) is not None

    def test_allows_skip_with_reason(self) -> None:
        """Verify skip with reason is not flagged."""
        code = '@pytest.mark.skip(reason="Not implemented")\ndef test_foo(): pass'
        skip_pattern = re.compile(
            r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE
        )

        assert skip_pattern.search(code) is None

    def test_detects_skipif_without_reason(self) -> None:
        """Verify detection of @pytest.mark.skipif without reason keyword."""
        code = '@pytest.mark.skipif(sys.platform == "win32")\ndef test_foo(): pass'

        # Pattern to match skipif without reason=
        skipif_no_reason = re.compile(
            r"@pytest\.mark\.skipif\s*\([^)]*\)\s*$", re.MULTILINE
        )

        match = skipif_no_reason.search(code)

        # Should find skipif without reason
        assert match is not None

    def test_allows_skipif_with_reason(self) -> None:
        """Verify skipif with reason is not flagged by strict pattern."""
        code = '@pytest.mark.skipif(sys.platform == "win32", reason="Windows only")'

        # This line has reason=, so we should NOT flag it
        assert "reason=" in code


class TestHardcodedPathDetection:
    """Self-tests for hardcoded path detection (fixes split bug)."""

    def test_detects_home_path(self) -> None:
        """Verify detection of /home/user paths."""
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = 'PATH = "/home/user/data"'

        assert re.search(pattern, line) is not None

    def test_detects_var_path(self) -> None:
        """Verify detection of /var paths."""
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = 'LOG_DIR = "/var/log/app"'

        assert re.search(pattern, line) is not None

    def test_correct_comment_detection(self) -> None:
        """Verify paths after # comment are ignored correctly.

        This test validates the FIX for the bug where line.split(pattern)
        was used instead of checking match.start() position.
        """
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        # Path is in a comment - should be ignored
        line = '# Example: PATH = "/home/user/data"'
        if match := re.search(pattern, line):
            comment_pos = line.find("#")
            # The CORRECT check: is the comment BEFORE the match?
            path_is_in_comment = comment_pos != -1 and comment_pos < match.start()
            assert path_is_in_comment, "Path in comment should be detected as such"

    def test_detects_path_before_inline_comment(self) -> None:
        """Verify paths BEFORE inline comments are still detected.

        This was a false negative in the original buggy code.
        """
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = 'PATH = "/home/user/thing"  # legit comment'
        match = re.search(pattern, line)

        assert match is not None

        # Comment is AFTER the match, so this should be flagged
        comment_pos = line.find("#")
        path_is_in_comment = comment_pos != -1 and comment_pos < match.start()

        assert not path_is_in_comment, "Path before comment should NOT be ignored"


class TestThinWrapperDetection:
    """Self-tests for thin wrapper detection."""

    def test_detects_simple_passthrough(self) -> None:
        """Verify detection of simple return-only wrappers."""
        code = """
def wrapper():
    return wrapped()
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.FunctionDef)

        # The body has one statement (Return with Call)
        assert len(func.body) == 1
        stmt = func.body[0]
        assert isinstance(stmt, ast.Return)
        assert isinstance(stmt.value, ast.Call)

    def test_detects_await_passthrough(self) -> None:
        """Verify detection of async return await wrappers."""
        code = """
async def wrapper():
    return await wrapped()
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.AsyncFunctionDef)

        stmt = func.body[0]
        assert isinstance(stmt, ast.Return)
        # The value is Await wrapping Call
        assert isinstance(stmt.value, ast.Await)
        assert isinstance(stmt.value.value, ast.Call)

    def test_ignores_wrapper_with_logic(self) -> None:
        """Verify wrappers with additional logic are not flagged."""
        code = """
def wrapper(x):
    if x:
        return wrapped()
    return None
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.FunctionDef)

        # Multiple statements = not a thin wrapper
        assert len(func.body) > 1

    def test_ignores_wrapper_with_transformation(self) -> None:
        """Verify wrappers that transform arguments are not flagged."""
        code = """
def wrapper(x):
    transformed = x.upper()
    return wrapped(transformed)
"""
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.FunctionDef)

        # Multiple statements = not a thin wrapper
        assert len(func.body) > 1


class TestComplexityDetection:
    """Self-tests for complexity detection."""

    def test_counts_if_as_branch(self) -> None:
        """Verify if statements are counted as branches."""
        code = """
def func():
    if x:
        pass
"""
        tree = ast.parse(code)

        branch_count = sum(1 for node in ast.walk(tree) if isinstance(node, ast.If))
        assert branch_count == 1

    def test_counts_for_as_branch(self) -> None:
        """Verify for loops are counted as branches."""
        code = """
def func():
    for x in items:
        pass
"""
        tree = ast.parse(code)

        branch_count = sum(1 for node in ast.walk(tree) if isinstance(node, ast.For))
        assert branch_count == 1

    def test_counts_and_or_as_branches(self) -> None:
        """Verify boolean operators are counted as branches."""
        code = """
def func():
    if x and y or z:
        pass
"""
        tree = ast.parse(code)

        branch_count = sum(
            1 for node in ast.walk(tree) if isinstance(node, (ast.And, ast.Or))
        )
        # One And, one Or
        assert branch_count == 2
