"""Tests for test file detection and coverage parsing.

Tests detect_tests and _is_test_file from analysis/testing.py.
Pure function tests with zero IO (coverage XML uses tmp_path).
All fields asserted. Exact count assertions.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from xml.etree.ElementTree import ParseError

import pytest

from tests.conftest import make_file_info, make_parsed_file
from unfamiliar.analysis.models import ParsedFile, ParsedImport
from unfamiliar.analysis.testing import (
    _is_test_file,
    _parse_coverage_xml,
    detect_tests,
)

if TYPE_CHECKING:
    from pathlib import Path


class TestIsTestFile:
    """Tests for _is_test_file helper function."""

    def test_tests_directory_returns_true(self):
        """File in tests/ directory is a test file."""
        assert _is_test_file("tests/utils.py") is True

    def test_test_prefix_returns_true(self):
        """File with test_ prefix is a test file."""
        assert _is_test_file("test_scoring.py") is True

    def test_test_suffix_returns_true(self):
        """File with _test suffix is a test file."""
        assert _is_test_file("utils_test.py") is True

    def test_spec_ts_returns_true(self):
        """TypeScript .spec.ts file is a test file."""
        assert _is_test_file("component.spec.ts") is True

    def test_spec_js_returns_true(self):
        """JavaScript .spec.js file is a test file."""
        assert _is_test_file("component.spec.js") is True

    def test_test_ts_returns_true(self):
        """TypeScript .test.ts file is a test file."""
        assert _is_test_file("component.test.ts") is True

    def test_test_js_returns_true(self):
        """JavaScript .test.js file is a test file."""
        assert _is_test_file("component.test.js") is True

    def test___tests___directory_returns_true(self):
        """File in __tests__/ directory is a test file."""
        assert _is_test_file("__tests__/utils.js") is True

    def test_spec_directory_returns_true(self):
        """File in spec/ directory is a test file."""
        assert _is_test_file("spec/api_spec.rb") is True

    def test_regular_file_returns_false(self):
        """Regular source file is not a test file."""
        assert _is_test_file("src/main.py") is False

    def test_setup_py_returns_false(self):
        """setup.py is not a test file."""
        assert _is_test_file("setup.py") is False

    def test_init_py_returns_false(self):
        """__init__.py is not a test file."""
        assert _is_test_file("__init__.py") is False

    def test_pyproject_toml_returns_false(self):
        """pyproject.toml is not a test file."""
        assert _is_test_file("pyproject.toml") is False


class TestDetectTests:
    """Tests for detect_tests function."""

    def test_no_test_files_empty_test_files_set(self, tmp_path: Path):
        """Repository with no test files has empty test_files set."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        assert result.test_files == set()

    def test_no_test_files_all_in_untested(self, tmp_path: Path):
        """All non-test files appear in untested_files."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        assert result.untested_files == {"src/main.py", "src/utils.py"}

    def test_no_coverage_tier_is_basic(self, tmp_path: Path):
        """Without coverage.xml, tier is 'basic'."""
        files = [make_file_info("src/main.py")]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        assert result.tier == "basic"

    def test_test_file_detected_by_convention(self, tmp_path: Path):
        """Test files are detected by naming convention."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("tests/test_main.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        assert len(result.test_files) == 1
        assert "tests/test_main.py" in result.test_files

    def test_source_to_tests_mapping(self, tmp_path: Path):
        """source_to_tests maps a source file to the test file that imports it.

        Uses an absolute import 'main' from tests/test_main.py, which
        _find_source_for_import resolves to main.py in the file list.
        """
        files = [
            make_file_info("main.py"),
            make_file_info("tests/test_main.py"),
        ]
        parsed = {
            "main.py": make_parsed_file("main.py"),
            "tests/test_main.py": ParsedFile(
                path="tests/test_main.py",
                language="python",
                imports=[
                    ParsedImport(
                        source_path="tests/test_main.py",
                        target="main",
                        is_relative=False,
                        names=["main"],
                    )
                ],
                definitions=[],
                entry_points=[],
                complexity=0,
                line_count=10,
            ),
        }

        result = detect_tests(files, parsed, tmp_path)

        assert result.source_to_tests == {"main.py": ["tests/test_main.py"]}

    def test_test_files_not_in_untested(self, tmp_path: Path):
        """Test files are excluded from untested_files."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("tests/test_main.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        assert "tests/test_main.py" not in result.untested_files

    def test_coverage_xml_sets_enhanced_tier(self, tmp_path: Path):
        """With coverage.xml, tier is 'enhanced'."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        xml_content = """<?xml version="1.0"?>
<coverage>
  <packages>
    <package>
      <classes>
        <class filename="src/main.py" line-rate="0.75"/>
        <class filename="src/utils.py" line-rate="0.50"/>
      </classes>
    </package>
  </packages>
</coverage>
"""
        (tmp_path / "coverage.xml").write_text(xml_content)

        result = detect_tests(files, parsed, tmp_path)

        assert result.tier == "enhanced"
        assert result.coverage_summary == {"src/main.py": 0.75, "src/utils.py": 0.50}


class TestParseCoverageXml:
    """Tests for _parse_coverage_xml helper function."""

    def test_valid_xml_returns_coverage_dict(self, tmp_path: Path):
        """Valid Cobertura XML returns coverage percentages."""
        xml_content = """<?xml version="1.0"?>
<coverage>
  <packages>
    <package>
      <classes>
        <class filename="src/main.py" line-rate="0.75"/>
        <class filename="src/utils.py" line-rate="0.50"/>
      </classes>
    </package>
  </packages>
</coverage>
"""
        coverage_path = tmp_path / "coverage.xml"
        coverage_path.write_text(xml_content)

        result = _parse_coverage_xml(coverage_path)

        assert len(result) == 2
        assert result["src/main.py"] == 0.75
        assert result["src/utils.py"] == 0.50

    def test_normalized_filename_strips_dot_slash(self, tmp_path: Path):
        """Filenames with ./ prefix are normalized."""
        xml_content = """<?xml version="1.0"?>
<coverage>
  <packages>
    <package>
      <classes>
        <class filename="./src/main.py" line-rate="1.0"/>
      </classes>
    </package>
  </packages>
</coverage>
"""
        coverage_path = tmp_path / "coverage.xml"
        coverage_path.write_text(xml_content)

        result = _parse_coverage_xml(coverage_path)

        assert "src/main.py" in result
        assert result["src/main.py"] == 1.0

    def test_empty_xml_returns_empty_dict(self, tmp_path: Path):
        """Empty/minimal XML returns empty dict."""
        xml_content = """<?xml version="1.0"?>
<coverage>
  <packages>
  </packages>
</coverage>
"""
        coverage_path = tmp_path / "coverage.xml"
        coverage_path.write_text(xml_content)

        result = _parse_coverage_xml(coverage_path)

        assert result == {}

    def test_invalid_xml_raises_parse_error(self, tmp_path: Path):
        """Malformed XML raises ET.ParseError."""
        coverage_path = tmp_path / "coverage.xml"
        coverage_path.write_text("not valid xml <")

        with pytest.raises(ParseError):  # ET.ParseError or similar
            _parse_coverage_xml(coverage_path)

    def test_file_with_zero_coverage(self, tmp_path: Path):
        """File with 0% coverage is included in results."""
        xml_content = """<?xml version="1.0"?>
<coverage>
  <packages>
    <package>
      <classes>
        <class filename="src/untested.py" line-rate="0.0"/>
      </classes>
    </package>
  </packages>
</coverage>
"""
        coverage_path = tmp_path / "coverage.xml"
        coverage_path.write_text(xml_content)

        result = _parse_coverage_xml(coverage_path)

        assert result["src/untested.py"] == 0.0


class TestAllFieldsAsserted:
    """Tests ensuring all TestInfo fields are asserted."""

    def test_all_testinfo_fields_populated(self, tmp_path: Path):
        """detect_tests returns TestInfo with all fields populated."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("tests/test_main.py"),
        ]
        parsed = {f.path: make_parsed_file(f.path) for f in files}

        result = detect_tests(files, parsed, tmp_path)

        # All fields should be present and of correct type
        assert isinstance(result.test_files, set)
        assert isinstance(result.source_to_tests, dict)
        assert isinstance(result.untested_files, set)
        assert result.tier in ("basic", "enhanced")
        assert isinstance(result.coverage_summary, dict)


class TestIsConfigFile:
    """Tests for _is_config_file helper function (GAP-008)."""

    @pytest.mark.parametrize(
        "path",
        [
            "setup.py",
            "src/setup.py",
            "setup.cfg",
            "pyproject.toml",
            "conftest.py",
            "tests/conftest.py",
            "__init__.py",
            "src/__init__.py",
            ".env",
            ".env.local",
            "config.yaml",
            "config.json",
            "src/config.py",
        ],
    )
    def test_config_patterns_return_true(self, path: str):
        """Known config file patterns return True."""
        from unfamiliar.analysis.testing import _is_config_file

        assert _is_config_file(path) is True

    @pytest.mark.parametrize(
        "path",
        [
            "src/main.py",
            "app.py",
            "utils.py",
            "test_foo.py",
            "README.md",
            "requirements.txt",
        ],
    )
    def test_non_config_patterns_return_false(self, path: str):
        """Non-config files return False."""
        from unfamiliar.analysis.testing import _is_config_file

        assert _is_config_file(path) is False


class TestResolveTestImport:
    """Tests for _resolve_test_import helper function (GAP-009)."""

    def test_level_1_relative_import(self):
        """Single-dot import resolves to same directory."""
        from unfamiliar.analysis.testing import _resolve_test_import

        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
            make_file_info("tests/test_main.py"),
        ]

        result = _resolve_test_import("src/tests/test_foo.py", ".utils", files)
        # Level 1 goes up one dir from src/tests, so base is src/
        # Then .utils -> src/utils.py
        assert result == "src/utils.py"

    def test_level_2_relative_import(self):
        """Double-dot import resolves two directories up."""
        from unfamiliar.analysis.testing import _resolve_test_import

        files = [
            make_file_info("src/core/engine.py"),
            make_file_info("src/utils.py"),
        ]

        result = _resolve_test_import("src/core/tests/test_engine.py", "..utils", files)
        # Level 2 goes up from src/core/tests to src/
        # Then ..utils -> src/utils.py
        assert result == "src/utils.py"

    def test_level_exceeds_path_depth_returns_none(self):
        """Import level greater than path depth returns None."""
        from unfamiliar.analysis.testing import _resolve_test_import

        files = [make_file_info("src/main.py")]

        # Path has only 2 parts (src, tests), but level=5
        result = _resolve_test_import("src/tests/test_foo.py", ".....module", files)
        assert result is None

    def test_empty_module_part_returns_none(self):
        """Import with only dots (no module) returns None."""
        from unfamiliar.analysis.testing import _resolve_test_import

        files = [make_file_info("src/main.py")]

        result = _resolve_test_import("src/tests/test_foo.py", "..", files)
        assert result is None

    def test_candidate_not_in_files_returns_none(self):
        """Import resolving to non-existent file returns None."""
        from unfamiliar.analysis.testing import _resolve_test_import

        files = [make_file_info("src/main.py")]

        result = _resolve_test_import("tests/test_foo.py", ".nonexistent", files)
        assert result is None


class TestFindSourceForImport:
    """Tests for _find_source_for_import helper function (GAP-010)."""

    def test_module_resolves_to_py_file(self):
        """Absolute import resolves to .py file."""
        from unfamiliar.analysis.testing import _find_source_for_import

        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
        ]

        result = _find_source_for_import("src.main", files)
        assert result == "src/main.py"

    def test_module_resolves_to_init_py(self):
        """Absolute import resolves to __init__.py for package."""
        from unfamiliar.analysis.testing import _find_source_for_import

        files = [
            make_file_info("src/__init__.py"),
            make_file_info("src/main.py"),
        ]

        result = _find_source_for_import("src", files)
        assert result == "src/__init__.py"

    def test_dotted_module_path_resolves(self):
        """Multi-segment import resolves to nested path."""
        from unfamiliar.analysis.testing import _find_source_for_import

        files = [
            make_file_info("unfamiliar/analysis/scoring.py"),
            make_file_info("unfamiliar/analysis/__init__.py"),
        ]

        result = _find_source_for_import("unfamiliar.analysis.scoring", files)
        assert result == "unfamiliar/analysis/scoring.py"

    def test_no_match_returns_none(self):
        """Import with no matching file returns None."""
        from unfamiliar.analysis.testing import _find_source_for_import

        files = [make_file_info("src/main.py")]

        result = _find_source_for_import("nonexistent.module", files)
        assert result is None

    def test_prefers_py_over_init(self):
        """When both exist, prefers module.py over module/__init__.py."""
        from unfamiliar.analysis.testing import _find_source_for_import

        files = [
            make_file_info("src/utils.py"),
            make_file_info("src/utils/__init__.py"),
        ]

        result = _find_source_for_import("src.utils", files)
        # Candidates are tried in order: .py first, then /__init__.py
        assert result == "src/utils.py"
