"""Tests for language adapters: TypeScriptAdapter, JavaScriptAdapter, PythonAdapter.

Tests are written against interface contracts from Phase 3 and Phase 4.
All adapters implement the LanguageAdapter protocol with:
- supported_extensions() -> frozenset[str]
- can_handle(path: str) -> bool
- parse_file(path: Path, content: bytes) -> ParsedFile
- resolve_import(...) -> str | None
"""

from __future__ import annotations

from pathlib import Path

from hypothesis import given, settings
from hypothesis import strategies as st

from unfamiliar.analysis.adapters import (
    JavaScriptAdapter,
    PythonAdapter,
    TypeScriptAdapter,
    create_default_registry,
)
from unfamiliar.analysis.adapters.protocol import AdapterRegistry
from unfamiliar.analysis.models import ParsedFile
from unfamiliar.constants import EntryType


class TestTypeScriptAdapterCanHandle:
    """Test TypeScriptAdapter.can_handle() extension detection."""

    def test_can_handle_ts_file_returns_true(self):
        """can_handle returns True for .ts files."""
        adapter = TypeScriptAdapter()
        assert adapter.can_handle("foo.ts") is True

    def test_can_handle_tsx_file_returns_true(self):
        """can_handle returns True for .tsx files."""
        adapter = TypeScriptAdapter()
        assert adapter.can_handle("foo.tsx") is True

    def test_can_handle_js_file_returns_false(self):
        """can_handle returns False for .js files (not TypeScript)."""
        adapter = TypeScriptAdapter()
        assert adapter.can_handle("foo.js") is False

    def test_can_handle_jsx_file_returns_false(self):
        """can_handle returns False for .jsx files (not TypeScript)."""
        adapter = TypeScriptAdapter()
        assert adapter.can_handle("foo.jsx") is False

    def test_can_handle_py_file_returns_false(self):
        """can_handle returns False for .py files."""
        adapter = TypeScriptAdapter()
        assert adapter.can_handle("foo.py") is False


class TestTypeScriptAdapterSupportedExtensions:
    """Test TypeScriptAdapter.supported_extensions()."""

    def test_supported_extensions_returns_ts_and_tsx(self):
        """supported_extensions returns frozenset with .ts and .tsx."""
        adapter = TypeScriptAdapter()
        extensions = adapter.supported_extensions()
        assert extensions == frozenset({".ts", ".tsx"})
        assert isinstance(extensions, frozenset)


class TestTypeScriptAdapterParseFile:
    """Test TypeScriptAdapter.parse_file() with various inputs."""

    def test_parse_file_valid_ts_returns_parsed_file_with_language(
        self,
    ):
        """parse_file with valid TS returns ParsedFile with language."""
        adapter = TypeScriptAdapter()
        content = b"import { foo } from './bar';\nconst x: number = 1;\n"
        result = adapter.parse_file(Path("src/app.ts"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "src/app.ts"
        assert result.language == "typescript"
        # 2 newlines means 3 lines
        assert result.line_count == 3

    def test_parse_file_extracts_imports(self):
        """parse_file extracts ES module imports."""
        adapter = TypeScriptAdapter()
        content = b"import { foo } from './utils';\nimport * as bar from 'external';\n"
        result = adapter.parse_file(Path("src/app.ts"), content)

        assert len(result.imports) == 2
        # Check first import (relative)
        assert result.imports[0].source_path == "src/app.ts"
        assert result.imports[0].target == "./utils"
        assert result.imports[0].is_relative is True
        assert "foo" in result.imports[0].names

        # Check second import (external)
        assert result.imports[1].target == "external"
        assert result.imports[1].is_relative is False

    def test_parse_file_extracts_definitions(self):
        """parse_file extracts function and class definitions."""
        adapter = TypeScriptAdapter()
        content = b"""
function myFunc(): void {}
class MyClass {
    method(): string { return "hello"; }
}
"""
        result = adapter.parse_file(Path("src/app.ts"), content)

        assert len(result.definitions) >= 2
        func_def = next((d for d in result.definitions if d.name == "myFunc"), None)
        assert func_def is not None
        assert func_def.kind == "function"
        assert func_def.line_start >= 1

        class_def = next((d for d in result.definitions if d.name == "MyClass"), None)
        assert class_def is not None
        assert class_def.kind == "class"

    def test_parse_file_with_syntax_error_returns_empty_lists(self):
        """parse_file with syntax error returns empty lists, no exception."""
        adapter = TypeScriptAdapter()
        content = b"function broken( { }\n"  # Invalid syntax
        result = adapter.parse_file(Path("src/broken.ts"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "src/broken.ts"
        assert result.language == "typescript"
        assert isinstance(result.imports, list)
        assert isinstance(result.definitions, list)

    def test_parse_file_tsx_uses_tsx_language(self):
        """parse_file with .tsx extension uses TSX language (handles JSX)."""
        adapter = TypeScriptAdapter()
        content = b"""
import React from 'react';
const Component = () => <div>Hello</div>;
export default Component;
"""
        result = adapter.parse_file(Path("src/Component.tsx"), content)

        assert result.path == "src/Component.tsx"
        assert result.language == "typescript"
        assert isinstance(result.definitions, list)

    def test_parse_file_all_fields_populated(self):
        """parse_file populates all ParsedFile fields correctly."""
        adapter = TypeScriptAdapter()
        content = b"import { x } from './y';\nfunction f() {}\n"
        result = adapter.parse_file(Path("app.ts"), content)

        assert result.path == "app.ts"
        assert result.language == "typescript"
        assert isinstance(result.imports, list)
        assert isinstance(result.definitions, list)
        assert isinstance(result.entry_points, list)
        assert isinstance(result.complexity, int)
        assert result.complexity >= 0
        assert isinstance(result.line_count, int)
        # 2 newlines means 3 lines
        assert result.line_count == 3

    def test_parse_file_empty_content(self):
        """parse_file handles empty content gracefully."""
        adapter = TypeScriptAdapter()
        result = adapter.parse_file(Path("empty.ts"), b"")

        assert isinstance(result, ParsedFile)
        assert result.path == "empty.ts"
        assert result.line_count == 0
        assert result.imports == []
        assert result.definitions == []


class TestTypeScriptAdapterResolveImport:
    """Test TypeScriptAdapter.resolve_import() for various import patterns."""

    def test_resolve_import_relative_returns_correct_path(self):
        """resolve_import for relative import returns correct resolved path."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/utils.ts", "src/utils.js", "src/app.ts"}
        result = adapter.resolve_import("src/app.ts", "./utils", repo_files)
        # TypeScript adapter tries .ts first
        assert result == "src/utils.ts"

    def test_resolve_import_bare_import_returns_none(self):
        """resolve_import for bare import (external package) returns None."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/utils.ts"}
        result = adapter.resolve_import("src/app.ts", "express", repo_files)
        assert result is None

    def test_resolve_import_path_alias_returns_none(self):
        """resolve_import for path alias returns None (known limitation)."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/components/Foo.ts"}
        result = adapter.resolve_import("src/app.ts", "@/components/Foo", repo_files)
        assert result is None

    def test_resolve_import_index_file(self):
        """resolve_import resolves to index file when appropriate."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/utils/index.ts", "src/app.ts"}
        result = adapter.resolve_import("src/app.ts", "./utils", repo_files)
        assert result == "src/utils/index.ts"

    def test_resolve_import_not_in_repo_returns_none(self):
        """resolve_import returns None when resolved path not in repo_files."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/app.ts"}
        result = adapter.resolve_import("src/app.ts", "./nonexistent", repo_files)
        assert result is None

    def test_resolve_import_prefers_ts_over_js(self):
        """resolve_import tries .ts extension before .js for TypeScript files."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/mod.ts", "src/mod.js"}
        result = adapter.resolve_import("src/app.ts", "./mod", repo_files)
        assert result == "src/mod.ts"

    def test_resolve_import_falls_back_to_js(self):
        """resolve_import falls back to .js if .ts not found."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/mod.js"}
        result = adapter.resolve_import("src/app.ts", "./mod", repo_files)
        assert result == "src/mod.js"

    def test_resolve_import_circular_reference(self):
        """resolve_import handles potential circular import scenarios."""
        adapter = TypeScriptAdapter()
        repo_files = {"a.ts", "b.ts"}
        # a imports b, b imports a - should resolve both
        result_a = adapter.resolve_import("a.ts", "./b", repo_files)
        result_b = adapter.resolve_import("b.ts", "./a", repo_files)

        assert result_a == "b.ts"
        assert result_b == "a.ts"

    def test_resolve_import_with_special_characters(self):
        """resolve_import handles paths with special characters."""
        adapter = TypeScriptAdapter()
        repo_files = {"src/@types/foo.d.ts", "src/app.ts"}
        # This is an external import starting with @
        result = adapter.resolve_import("src/app.ts", "@types/foo", repo_files)
        # Bare import starting with @ should return None (external)
        assert result is None


class TestJavaScriptAdapterCanHandle:
    """Test JavaScriptAdapter.can_handle() extension detection."""

    def test_can_handle_js_file_returns_true(self):
        """can_handle returns True for .js files."""
        adapter = JavaScriptAdapter()
        assert adapter.can_handle("foo.js") is True

    def test_can_handle_jsx_file_returns_true(self):
        """can_handle returns True for .jsx files."""
        adapter = JavaScriptAdapter()
        assert adapter.can_handle("foo.jsx") is True

    def test_can_handle_ts_file_returns_false(self):
        """can_handle returns False for .ts files (not JavaScript)."""
        adapter = JavaScriptAdapter()
        assert adapter.can_handle("foo.ts") is False

    def test_can_handle_tsx_file_returns_false(self):
        """can_handle returns False for .tsx files (not JavaScript)."""
        adapter = JavaScriptAdapter()
        assert adapter.can_handle("foo.tsx") is False

    def test_can_handle_py_file_returns_false(self):
        """can_handle returns False for .py files."""
        adapter = JavaScriptAdapter()
        assert adapter.can_handle("foo.py") is False


class TestJavaScriptAdapterSupportedExtensions:
    """Test JavaScriptAdapter.supported_extensions()."""

    def test_supported_extensions_returns_js_and_jsx(self):
        """supported_extensions returns frozenset with .js and .jsx."""
        adapter = JavaScriptAdapter()
        extensions = adapter.supported_extensions()
        assert extensions == frozenset({".js", ".jsx"})
        assert isinstance(extensions, frozenset)


class TestJavaScriptAdapterParseFile:
    """Test JavaScriptAdapter.parse_file() with various inputs."""

    def test_parse_file_with_require_captures_commonjs_imports(self):
        """parse_file with require() captures CommonJS imports."""
        adapter = JavaScriptAdapter()
        content = b"const utils = require('./utils');\n"
        result = adapter.parse_file(Path("src/app.js"), content)

        assert len(result.imports) == 1
        assert result.imports[0].target == "./utils"
        assert result.imports[0].is_relative is True
        assert result.imports[0].source_path == "src/app.js"

    def test_parse_file_with_es_and_commonjs_combined(self):
        """parse_file with both ES and CommonJS imports combines in imports list."""
        adapter = JavaScriptAdapter()
        content = b"""
import { foo } from './es-module';
const bar = require('./commonjs-module');
"""
        result = adapter.parse_file(Path("src/app.js"), content)

        assert len(result.imports) == 2
        targets = {imp.target for imp in result.imports}
        assert "./es-module" in targets
        assert "./commonjs-module" in targets

    def test_parse_file_valid_js_returns_parsed_file(self):
        """parse_file with valid JS returns ParsedFile with language='javascript'."""
        adapter = JavaScriptAdapter()
        # 1 newline means 2 lines
        content = b"const x = 1;\nfunction foo() { return x; }"
        result = adapter.parse_file(Path("src/app.js"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "src/app.js"
        assert result.language == "javascript"
        # 1 newline means 2 lines
        assert result.line_count == 2

    def test_parse_file_with_syntax_error_returns_empty_lists(self):
        """parse_file with syntax error returns empty lists, no exception."""
        adapter = JavaScriptAdapter()
        content = b"function broken( { }\n"
        result = adapter.parse_file(Path("src/broken.js"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "src/broken.js"
        assert result.language == "javascript"
        assert isinstance(result.imports, list)
        assert isinstance(result.definitions, list)

    def test_parse_file_detects_module_exports_entry_point(self):
        """parse_file detects module.exports as entry point."""
        adapter = JavaScriptAdapter()
        content = b"module.exports = { foo: 1 };\n"
        result = adapter.parse_file(Path("src/app.js"), content)

        assert len(result.entry_points) >= 1
        main_ep = next(
            (ep for ep in result.entry_points if ep.entry_type == EntryType.MAIN), None
        )
        assert main_ep is not None
        assert "module.exports" in main_ep.description

    def test_parse_file_all_fields_populated(self):
        """parse_file populates all ParsedFile fields correctly."""
        adapter = JavaScriptAdapter()
        # 1 newline means 2 lines
        content = b"const x = require('./y');\nfunction f() {}"
        result = adapter.parse_file(Path("app.js"), content)

        assert result.path == "app.js"
        assert result.language == "javascript"
        assert isinstance(result.imports, list)
        assert len(result.imports) == 1
        assert isinstance(result.definitions, list)
        assert isinstance(result.entry_points, list)
        assert isinstance(result.complexity, int)
        assert result.complexity >= 0
        assert isinstance(result.line_count, int)
        # 1 newline means 2 lines
        assert result.line_count == 2

    def test_parse_file_very_long_line(self):
        """parse_file handles very long lines without crashing."""
        adapter = JavaScriptAdapter()
        content = b"const x = '" + b"x" * 10000 + b"';\n"
        result = adapter.parse_file(Path("long.js"), content)

        assert isinstance(result, ParsedFile)
        # One newline means 2 lines
        assert result.line_count == 2

    def test_parse_file_mixed_line_endings(self):
        """parse_file handles mixed line endings (CRLF/LF)."""
        adapter = JavaScriptAdapter()
        content = b"const a = 1;\r\nconst b = 2;\nconst c = 3;\r\n"
        result = adapter.parse_file(Path("mixed.js"), content)

        assert isinstance(result, ParsedFile)
        # Line count may vary based on how line endings are counted
        # But should be at least 3 lines
        assert result.line_count == 4


class TestJavaScriptAdapterResolveImport:
    """Test JavaScriptAdapter.resolve_import() extension order."""

    def test_resolve_import_js_tried_before_ts(self):
        """resolve_import tries .js extension before .ts for JavaScript files."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/mod.js", "src/mod.ts"}
        result = adapter.resolve_import("src/app.js", "./mod", repo_files)
        assert result == "src/mod.js"

    def test_resolve_import_falls_back_to_ts(self):
        """resolve_import falls back to .ts if .js not found."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/mod.ts"}
        result = adapter.resolve_import("src/app.js", "./mod", repo_files)
        assert result == "src/mod.ts"

    def test_resolve_import_bare_returns_none(self):
        """resolve_import for bare import returns None."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/app.js"}
        result = adapter.resolve_import("src/app.js", "express", repo_files)
        assert result is None

    def test_resolve_import_relative_returns_correct_path(self):
        """resolve_import for relative import returns correct resolved path."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/utils.js", "src/utils.ts", "src/app.js"}
        result = adapter.resolve_import("src/app.js", "./utils", repo_files)
        # JavaScript adapter tries .js first
        assert result == "src/utils.js"

    def test_resolve_import_index_file(self):
        """resolve_import resolves to index file when appropriate."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/utils/index.js", "src/app.js"}
        result = adapter.resolve_import("src/app.js", "./utils", repo_files)
        assert result == "src/utils/index.js"

    def test_resolve_import_not_in_repo_returns_none(self):
        """resolve_import returns None when resolved path not in repo_files."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/app.js"}
        result = adapter.resolve_import("src/app.js", "./nonexistent", repo_files)
        assert result is None

    def test_resolve_import_path_alias_returns_none(self):
        """resolve_import for path alias returns None (known limitation)."""
        adapter = JavaScriptAdapter()
        repo_files = {"src/components/Foo.js"}
        result = adapter.resolve_import("src/app.js", "@/components/Foo", repo_files)
        assert result is None


class TestPythonAdapterCanHandle:
    """Test PythonAdapter.can_handle() extension detection."""

    def test_can_handle_py_file_returns_true(self):
        """can_handle returns True for .py files."""
        adapter = PythonAdapter()
        assert adapter.can_handle("foo.py") is True

    def test_can_handle_ts_file_returns_false(self):
        """can_handle returns False for .ts files."""
        adapter = PythonAdapter()
        assert adapter.can_handle("foo.ts") is False

    def test_can_handle_js_file_returns_false(self):
        """can_handle returns False for .js files."""
        adapter = PythonAdapter()
        assert adapter.can_handle("foo.js") is False

    def test_can_handle_jsx_file_returns_false(self):
        """can_handle returns False for .jsx files."""
        adapter = PythonAdapter()
        assert adapter.can_handle("foo.jsx") is False


class TestPythonAdapterSupportedExtensions:
    """Test PythonAdapter.supported_extensions()."""

    def test_supported_extensions_returns_py(self):
        """supported_extensions returns frozenset with .py."""
        adapter = PythonAdapter()
        extensions = adapter.supported_extensions()
        assert extensions == frozenset({".py"})
        assert isinstance(extensions, frozenset)


class TestPythonAdapterParseFile:
    """Test PythonAdapter.parse_file() with various inputs."""

    def test_parse_file_valid_python_returns_parsed_file(self):
        """parse_file with valid Python returns ParsedFile with language='python'."""
        adapter = PythonAdapter()
        content = b"import os\nx = 1\n"
        result = adapter.parse_file(Path("src/app.py"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "src/app.py"
        assert result.language == "python"
        # 2 newlines means 3 lines
        assert result.line_count == 3

    def test_parse_file_line_count_accurate(self):
        """parse_file sets line_count correctly."""
        adapter = PythonAdapter()
        # 4 newlines means 5 lines (count + 1)
        content = b"line1\nline2\nline3\nline4\n"
        result = adapter.parse_file(Path("app.py"), content)
        assert result.line_count == 5

    def test_parse_file_with_syntax_error_returns_empty_lists(self):
        """parse_file with syntax error returns empty lists, no exception."""
        adapter = PythonAdapter()
        content = b"def broken(:\n"  # Invalid syntax - 1 newline
        result = adapter.parse_file(Path("broken.py"), content)

        assert isinstance(result, ParsedFile)
        assert result.path == "broken.py"
        assert result.language == "python"
        # On syntax error, imports and definitions should be empty
        assert result.imports == []
        assert result.definitions == []
        # But line_count should still be set (1 newline = 2 lines)
        assert result.line_count == 2

    def test_parse_file_extracts_imports(self):
        """parse_file extracts Python import statements."""
        adapter = PythonAdapter()
        content = b"import os\nfrom pathlib import Path\n"
        result = adapter.parse_file(Path("app.py"), content)

        imp = result.imports[0]
        assert imp.source_path == "app.py"
        assert imp.is_relative is False
        assert imp.names == ["os"]

        imp2 = result.imports[1]
        assert imp2.target == "pathlib"
        assert imp2.is_relative is False
        assert "Path" in imp2.names

        assert len(result.imports) == 2
        targets = [imp.target for imp in result.imports]
        assert "os" in targets
        assert "pathlib" in targets

    def test_parse_file_extracts_definitions(self):
        """parse_file extracts function and class definitions."""
        adapter = PythonAdapter()
        content = b"""
def my_func():
    pass

class MyClass:
    def method(self):
        pass
"""
        result = adapter.parse_file(Path("app.py"), content)

        assert len(result.definitions) == 3

        func_def = next((d for d in result.definitions if d.name == "my_func"), None)
        assert func_def is not None
        assert func_def.kind == "function"
        assert func_def.line_start == 2
        assert func_def.line_end == 3
        assert func_def.is_async is False

        class_def = next((d for d in result.definitions if d.name == "MyClass"), None)
        assert class_def is not None
        assert class_def.kind == "class"
        assert class_def.line_start == 5
        assert class_def.line_end == 7

    def test_parse_file_all_fields_populated(self):
        """parse_file populates all ParsedFile fields correctly."""
        adapter = PythonAdapter()
        content = b"import os\ndef f():\n    if x:\n        pass\n"
        result = adapter.parse_file(Path("app.py"), content)

        assert result.path == "app.py"
        assert result.language == "python"
        assert isinstance(result.imports, list)
        assert len(result.imports) == 1
        assert isinstance(result.definitions, list)
        assert isinstance(result.entry_points, list)
        assert isinstance(result.complexity, int)
        assert result.complexity == 1
        assert isinstance(result.line_count, int)
        # 2 newlines means 3 lines
        assert result.line_count == 5

        func_def = result.definitions[0]
        assert func_def.line_start >= 1
        assert func_def.line_end >= func_def.line_start

    def test_parse_file_extracts_entry_points(self):
        """parse_file extracts entry points from if __name__ == '__main__'."""
        adapter = PythonAdapter()
        content = b'def main():\n    pass\n\nif __name__ == "__main__":\n    main()\n'
        result = adapter.parse_file(Path("app.py"), content)

        assert len(result.entry_points) == 1
        ep = result.entry_points[0]
        assert ep.file_path == "app.py"
        assert ep.entry_type == EntryType.MAIN
        assert ep.line == 4
        assert "__name__" in ep.description or "main" in ep.description

    def test_parse_file_unicode_content(self):
        """parse_file handles Unicode content correctly."""
        adapter = PythonAdapter()
        # Unicode string in Python source - 2 newlines means 3 lines
        content = b"# -*- coding: utf-8 -*-\nmsg = 'Hello, World'\n"
        result = adapter.parse_file(Path("unicode.py"), content)

        assert isinstance(result, ParsedFile)
        assert result.language == "python"
        # 2 newlines means 3 lines
        assert result.line_count == 3


class TestPythonAdapterResolveImport:
    """Test PythonAdapter.resolve_import() for various import patterns."""

    def test_resolve_import_relative_import(self):
        """resolve_import handles relative imports."""
        adapter = PythonAdapter()
        repo_files = {"src/utils.py", "src/app.py"}
        result = adapter.resolve_import("src/app.py", ".utils", repo_files)
        assert result == "src/utils.py"

    def test_resolve_import_absolute_import(self):
        """resolve_import handles absolute imports."""
        adapter = PythonAdapter()
        repo_files = {"myapp/utils.py"}
        result = adapter.resolve_import("myapp/app.py", "myapp.utils", repo_files)
        assert result == "myapp/utils.py"

    def test_resolve_import_external_returns_none(self):
        """resolve_import returns None for external packages."""
        adapter = PythonAdapter()
        repo_files = {"src/app.py"}
        result = adapter.resolve_import("src/app.py", "os", repo_files)
        assert result is None

    def test_resolve_import_parent_directory(self):
        """resolve_import handles .. parent imports."""
        adapter = PythonAdapter()
        repo_files = {"src/utils.py", "src/subdir/app.py"}
        result = adapter.resolve_import("src/subdir/app.py", "..utils", repo_files)
        assert result == "src/utils.py"

    def test_resolve_import_grandparent_directory(self):
        """resolve_import handles ... grandparent imports."""
        adapter = PythonAdapter()
        repo_files = {"src/utils.py", "src/a/b/app.py"}
        result = adapter.resolve_import("src/a/b/app.py", "...utils", repo_files)
        assert result == "src/utils.py"

    def test_resolve_import_package_init(self):
        """resolve_import resolves to __init__.py for package imports."""
        adapter = PythonAdapter()
        repo_files = {"myapp/__init__.py", "myapp/sub.py"}
        result = adapter.resolve_import("other/file.py", "myapp", repo_files)
        assert result == "myapp/__init__.py"

    def test_resolve_import_submodule(self):
        """resolve_import handles submodule imports."""
        adapter = PythonAdapter()
        repo_files = {"myapp/utils/helpers.py", "other/file.py"}
        result = adapter.resolve_import(
            "other/file.py", "myapp.utils.helpers", repo_files
        )
        assert result == "myapp/utils/helpers.py"

    def test_resolve_import_partial_match_returns_none(self):
        """resolve_import returns None when no matching file exists."""
        adapter = PythonAdapter()
        repo_files = {"src/app.py"}
        result = adapter.resolve_import("src/app.py", ".nonexistent", repo_files)
        assert result is None

    def test_resolve_import_relative_with_submodules(self):
        """resolve_import handles single-dot relative imports with submodules."""
        adapter = PythonAdapter()
        repo_files = {"src/handlers/services/auth/login.py", "src/handlers/api.py"}
        # .services from src/handlers/api.py -> src/handlers/services/...
        result = adapter.resolve_import(
            "src/handlers/api.py", ".services.auth.login", repo_files
        )
        assert result == "src/handlers/services/auth/login.py"

    def test_resolve_import_parent_with_submodules(self):
        """resolve_import handles double-dot parent imports with submodules."""
        adapter = PythonAdapter()
        repo_files = {"src/services/auth/login.py", "src/handlers/api.py"}
        # ..services from src/handlers/api.py -> go up to src, then into services
        result = adapter.resolve_import(
            "src/handlers/api.py", "..services.auth.login", repo_files
        )
        assert result == "src/services/auth/login.py"


class TestAdapterRegistry:
    """Test AdapterRegistry registration and dispatch."""

    def test_create_default_registry_has_three_adapters(self):
        """create_default_registry returns registry with 3 adapters registered."""
        registry = create_default_registry()

        languages = registry.supported_languages()
        assert len(languages) == 3
        assert "python" in languages
        assert "typescript" in languages
        assert "javascript" in languages

    def test_get_adapter_py_returns_python_adapter(self):
        """get_adapter returns PythonAdapter for .py files."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.py")

        assert adapter is not None
        assert isinstance(adapter, PythonAdapter)

    def test_get_adapter_ts_returns_typescript_adapter(self):
        """get_adapter returns TypeScriptAdapter for .ts files."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.ts")

        assert adapter is not None
        assert isinstance(adapter, TypeScriptAdapter)

    def test_get_adapter_tsx_returns_typescript_adapter(self):
        """get_adapter returns TypeScriptAdapter for .tsx files."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.tsx")

        assert adapter is not None
        assert isinstance(adapter, TypeScriptAdapter)

    def test_get_adapter_js_returns_javascript_adapter(self):
        """get_adapter returns JavaScriptAdapter for .js files."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.js")

        assert adapter is not None
        assert isinstance(adapter, JavaScriptAdapter)

    def test_get_adapter_jsx_returns_javascript_adapter(self):
        """get_adapter returns JavaScriptAdapter for .jsx files."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.jsx")

        assert adapter is not None
        assert isinstance(adapter, JavaScriptAdapter)

    def test_get_adapter_unsupported_returns_none(self):
        """get_adapter returns None for unsupported file extensions."""
        registry = create_default_registry()
        adapter = registry.get_adapter("file.rb")

        assert adapter is None

    def test_supported_languages_contains_expected_keys(self):
        """supported_languages contains expected adapter names as keys."""
        registry = create_default_registry()
        languages = registry.supported_languages()

        assert "python" in languages
        assert "typescript" in languages
        assert "javascript" in languages

    def test_supported_languages_extensions_correct(self):
        """supported_languages returns correct extensions for each adapter."""
        registry = create_default_registry()
        languages = registry.supported_languages()

        assert languages["python"] == frozenset({".py"})
        assert languages["typescript"] == frozenset({".ts", ".tsx"})
        assert languages["javascript"] == frozenset({".js", ".jsx"})

    def test_register_adds_adapter(self):
        """register adds an adapter to the registry."""
        registry = AdapterRegistry()
        adapter = PythonAdapter()
        registry.register(adapter)

        assert registry.get_adapter("test.py") is not None

    def test_get_adapter_respects_registration_order(self):
        """get_adapter returns first matching adapter when multiple match."""
        registry = AdapterRegistry()
        registry.register(PythonAdapter())
        registry.register(TypeScriptAdapter())

        # Python was registered first, so .ts file won't match Python
        # and will match TypeScript
        result = registry.get_adapter("file.ts")
        assert isinstance(result, TypeScriptAdapter)


# Predefined test cases for adapter dispatch
_ADAPTER_TEST_CASES = [
    ("file.py", "PythonAdapter"),
    ("file.ts", "TypeScriptAdapter"),
    ("file.tsx", "TypeScriptAdapter"),
    ("file.js", "JavaScriptAdapter"),
    ("file.jsx", "JavaScriptAdapter"),
    ("src/utils.py", "PythonAdapter"),
    ("src/components/Button.tsx", "TypeScriptAdapter"),
    ("lib/helper.js", "JavaScriptAdapter"),
]


class TestPropertyBased:
    """Property-based tests using Hypothesis."""

    @given(st.sampled_from(_ADAPTER_TEST_CASES))
    @settings(max_examples=50)
    def test_registry_dispatch_always_returns_correct_adapter(self, path_and_type):
        """Property: get_adapter returns correct adapter for all extensions."""
        path, expected_type = path_and_type
        registry = create_default_registry()
        adapter = registry.get_adapter(path)

        assert adapter is not None, f"Expected adapter for {path}"
        assert type(adapter).__name__ == expected_type
        assert adapter.can_handle(path) is True

    @given(
        repo_files=st.frozensets(
            st.sampled_from(
                [
                    "src/a.ts",
                    "src/b.ts",
                    "src/c.ts",
                    "src/utils.ts",
                    "src/lib.ts",
                    "lib/helper.ts",
                ]
            ),
            min_size=1,
            max_size=6,
        ),
        filename=st.sampled_from(["src/app.ts", "src/main.ts", "src/index.ts"]),
    )
    @settings(max_examples=30)
    def test_resolved_import_exists_in_repo_files(self, repo_files, filename):
        """Property: resolved import path always in repo_files if not None."""
        adapter = TypeScriptAdapter()
        # Generate import targets from the repo files
        for target_file in list(repo_files)[:3]:
            # Create a relative import path
            import_target = "./" + target_file.split("/")[-1].replace(".ts", "")
            result = adapter.resolve_import(filename, import_target, set(repo_files))
            if result is not None:
                assert result in repo_files, (
                    f"Resolved {import_target} to {result} but not in repo_files"
                )


class TestPythonAdapterDetectAppType:
    """Tests for PythonAdapter entry point detection via _detect_app_type."""

    def test_flask_app_detected(self):
        """Flask(__name__) is detected as API_ROUTE entry point."""
        adapter = PythonAdapter()
        content = b"from flask import Flask\napp = Flask(__name__)\n"
        result = adapter.parse_file(Path("server.py"), content)

        assert len(result.entry_points) >= 1
        flask_eps = [
            ep for ep in result.entry_points if ep.entry_type == EntryType.API_ROUTE
        ]
        assert len(flask_eps) == 1
        assert "app" in flask_eps[0].description.lower()

    def test_fastapi_app_detected(self):
        """FastAPI() is detected as API_ROUTE entry point."""
        adapter = PythonAdapter()
        content = b"from fastapi import FastAPI\napp = FastAPI()\n"
        result = adapter.parse_file(Path("main.py"), content)

        api_eps = [
            ep for ep in result.entry_points if ep.entry_type == EntryType.API_ROUTE
        ]
        assert len(api_eps) == 1

    def test_typer_app_detected(self):
        """typer.Typer() is detected as CLI entry point."""
        adapter = PythonAdapter()
        content = b"import typer\napp = typer.Typer()\n"
        result = adapter.parse_file(Path("cli.py"), content)

        cli_eps = [ep for ep in result.entry_points if ep.entry_type == EntryType.CLI]
        assert len(cli_eps) == 1

    def test_django_urlpatterns_not_detected_as_app(self):
        """Plain assignment (not a recognized constructor) returns no app entry."""
        adapter = PythonAdapter()
        content = b"urlpatterns = [path('/', views.index)]\n"
        result = adapter.parse_file(Path("urls.py"), content)

        # urlpatterns is not detected via _detect_app_type (it's an assignment
        # to a list, not a recognized constructor call)
        app_eps = [
            ep
            for ep in result.entry_points
            if ep.entry_type in (EntryType.API_ROUTE, EntryType.CLI)
        ]
        assert len(app_eps) == 0

    def test_no_app_type_for_plain_python(self):
        """Plain Python with no app patterns produces no app entry points."""
        adapter = PythonAdapter()
        content = b"def add(a, b):\n    return a + b\n"
        result = adapter.parse_file(Path("utils.py"), content)

        app_eps = [
            ep
            for ep in result.entry_points
            if ep.entry_type in (EntryType.API_ROUTE, EntryType.CLI)
        ]
        assert len(app_eps) == 0

    def test_flask_with_attribute_access_detected(self):
        """flask.Flask(__name__) via attribute access is detected."""
        adapter = PythonAdapter()
        content = b"import flask\napp = flask.Flask(__name__)\n"
        result = adapter.parse_file(Path("app.py"), content)

        api_eps = [
            ep for ep in result.entry_points if ep.entry_type == EntryType.API_ROUTE
        ]
        assert len(api_eps) == 1
