"""Tests for JavaScript/TypeScript import resolver.

These tests verify that the ImportResolver correctly resolves import paths
to actual file paths, enabling multi-file context extraction.
"""

import pytest

from codeflash.languages.javascript.import_resolver import HelperSearchContext, ImportResolver, MultiFileHelperFinder
from codeflash.languages.javascript.treesitter import ImportInfo


class TestImportResolver:
    """Tests for ImportResolver class."""

    @pytest.fixture
    def project_root(self, tmp_path):
        """Create a temporary project structure."""
        # Create directories
        src_dir = tmp_path / "src"
        src_dir.mkdir()
        lib_dir = src_dir / "lib"
        lib_dir.mkdir()
        utils_dir = src_dir / "utils"
        utils_dir.mkdir()

        # Create some test files
        (src_dir / "main.ts").write_text("export function main() {}")
        (src_dir / "helper.ts").write_text("export function helper() {}")
        (lib_dir / "math.ts").write_text("export function add() {}")
        (utils_dir / "index.ts").write_text("export function util() {}")

        return tmp_path

    @pytest.fixture
    def resolver(self, project_root):
        """Create an ImportResolver for the project."""
        return ImportResolver(project_root)

    def test_is_external_package_lodash(self, resolver):
        """Test that bare imports are detected as external."""
        assert resolver._is_external_package("lodash") is True

    def test_is_external_package_scoped(self, resolver):
        """Test that scoped packages are detected as external."""
        assert resolver._is_external_package("@company/utils") is True

    def test_is_external_package_react(self, resolver):
        """Test that react is detected as external."""
        assert resolver._is_external_package("react") is True

    def test_is_not_external_package_relative(self, resolver):
        """Test that relative imports are not external."""
        assert resolver._is_external_package("./utils") is False

    def test_is_not_external_package_parent_relative(self, resolver):
        """Test that parent relative imports are not external."""
        assert resolver._is_external_package("../lib/math") is False

    def test_resolve_relative_import_same_dir(self, resolver, project_root):
        """Test resolving ./helper from same directory."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./helper",
            default_import=None,
            named_imports=[("helper", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "helper.ts"
        assert result.module_path == "./helper"

    def test_resolve_relative_import_subdirectory(self, resolver, project_root):
        """Test resolving ./lib/math from parent directory."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./lib/math",
            default_import=None,
            named_imports=[("add", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "lib" / "math.ts"

    def test_resolve_index_file(self, resolver, project_root):
        """Test resolving ./utils to ./utils/index.ts."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./utils",
            default_import=None,
            named_imports=[("util", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "utils" / "index.ts"

    def test_resolve_external_package_returns_none(self, resolver, project_root):
        """Test that external package imports return None."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="lodash",
            default_import="_",
            named_imports=[],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is None

    def test_resolve_nonexistent_file_returns_none(self, resolver, project_root):
        """Test that nonexistent file imports return None."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./nonexistent",
            default_import=None,
            named_imports=[("foo", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is None

    def test_resolve_with_explicit_extension(self, resolver, project_root):
        """Test resolving import with explicit .ts extension."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./helper.ts",
            default_import=None,
            named_imports=[("helper", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "helper.ts"

    def test_resolved_import_contains_imported_names(self, resolver, project_root):
        """Test that ResolvedImport contains correct imported names."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./helper",
            default_import="Helper",
            named_imports=[("foo", None), ("bar", "baz")],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert "Helper" in result.imported_names
        assert "foo" in result.imported_names
        assert "baz" in result.imported_names  # alias is used
        assert result.is_default_import is True

    def test_namespace_import_detection(self, resolver, project_root):
        """Test that namespace imports are correctly detected."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./helper",
            default_import=None,
            named_imports=[],
            namespace_import="utils",
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.is_namespace_import is True
        assert result.namespace_name == "utils"

    def test_caching_works(self, resolver, project_root):
        """Test that resolution results are cached."""
        source_file = project_root / "src" / "main.ts"
        import_info = ImportInfo(
            module_path="./helper",
            default_import=None,
            named_imports=[("helper", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        # First resolution
        result1 = resolver.resolve_import(import_info, source_file)
        # Second resolution should use cache
        result2 = resolver.resolve_import(import_info, source_file)

        assert result1 is not None
        assert result2 is not None
        assert result1.file_path == result2.file_path
        # Check cache was populated
        assert (source_file, "./helper") in resolver._resolution_cache


class TestMultiFileHelperFinder:
    """Tests for MultiFileHelperFinder class."""

    @pytest.fixture
    def project_root(self, tmp_path):
        """Create a temporary project with multi-file structure."""
        src_dir = tmp_path / "src"
        src_dir.mkdir()

        # Main file that imports helper
        (src_dir / "main.ts").write_text("""
import { helperFunc } from './helper';

export function mainFunc() {
    return helperFunc() + 1;
}
""")

        # Helper file
        (src_dir / "helper.ts").write_text("""
export function helperFunc() {
    return 42;
}

export function unusedHelper() {
    return 0;
}
""")

        return tmp_path

    @pytest.fixture
    def resolver(self, project_root):
        """Create an ImportResolver."""
        return ImportResolver(project_root)

    @pytest.fixture
    def finder(self, project_root, resolver):
        """Create a MultiFileHelperFinder."""
        return MultiFileHelperFinder(project_root, resolver)

    def test_helper_search_context_defaults(self):
        """Test HelperSearchContext default values."""
        context = HelperSearchContext()
        assert context.visited_files == set()
        assert context.visited_functions == set()
        assert context.current_depth == 0
        assert context.max_depth == 2


class TestExportInfo:
    """Tests for ExportInfo parsing in TreeSitterAnalyzer."""

    @pytest.fixture
    def js_analyzer(self):
        """Create a JavaScript analyzer."""
        from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage

        return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)

    def test_find_named_export_function(self, js_analyzer):
        """Test finding export function declaration."""
        code = "export function helper() { return 1; }"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("helper", None) in exports[0].exported_names
        assert exports[0].is_reexport is False

    def test_find_default_export_function(self, js_analyzer):
        """Test finding export default function."""
        code = "export default function myFunc() { return 1; }"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].default_export == "myFunc"

    def test_find_export_declaration(self, js_analyzer):
        """Test finding export { name }."""
        code = """
function helper() { return 1; }
export { helper };
"""
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("helper", None) in exports[0].exported_names

    def test_find_export_with_alias(self, js_analyzer):
        """Test finding export { name as alias }."""
        code = """
function helper() { return 1; }
export { helper as myHelper };
"""
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("helper", "myHelper") in exports[0].exported_names

    def test_find_reexport(self, js_analyzer):
        """Test finding re-export from another module."""
        code = "export { helper } from './other';"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].is_reexport is True
        assert exports[0].reexport_source == "./other"

    def test_find_export_const(self, js_analyzer):
        """Test finding export const declaration."""
        code = "export const myVar = 42;"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("myVar", None) in exports[0].exported_names

    def test_is_function_exported_true(self, js_analyzer):
        """Test is_function_exported returns True for exported function."""
        code = "export function helper() { return 1; }"
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is True
        assert export_name == "helper"

    def test_is_function_exported_false(self, js_analyzer):
        """Test is_function_exported returns False for non-exported function."""
        code = "function helper() { return 1; }"
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is False
        assert export_name is None

    def test_is_function_exported_with_alias(self, js_analyzer):
        """Test is_function_exported returns alias name."""
        code = """
function helper() { return 1; }
export { helper as myHelper };
"""
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is True
        assert export_name == "myHelper"

    def test_is_function_exported_default(self, js_analyzer):
        """Test is_function_exported returns 'default' for default export."""
        code = "export default function helper() { return 1; }"
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is True
        assert export_name == "default"


class TestCommonJSRequire:
    """Tests for CommonJS require() import parsing."""

    @pytest.fixture
    def js_analyzer(self):
        """Create a JavaScript analyzer."""
        from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage

        return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)

    def test_require_default_import(self, js_analyzer):
        """Test const foo = require('./module')."""
        code = "const helper = require('./helper');"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./helper"
        assert imports[0].default_import == "helper"
        assert imports[0].named_imports == []

    def test_require_destructured_import(self, js_analyzer):
        """Test const { a, b } = require('./module')."""
        code = "const { foo, bar } = require('./helper');"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./helper"
        assert imports[0].default_import is None
        assert ("foo", None) in imports[0].named_imports
        assert ("bar", None) in imports[0].named_imports

    def test_require_destructured_with_alias(self, js_analyzer):
        """Test const { a: aliasA } = require('./module')."""
        code = "const { foo: myFoo, bar } = require('./helper');"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./helper"
        assert ("foo", "myFoo") in imports[0].named_imports
        assert ("bar", None) in imports[0].named_imports

    def test_require_property_access(self, js_analyzer):
        """Test const foo = require('./module').bar."""
        code = "const myFunc = require('./helper').helperFunc;"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./helper"
        assert imports[0].default_import is None
        # helperFunc is imported and assigned to myFunc
        assert ("helperFunc", "myFunc") in imports[0].named_imports

    def test_require_property_access_same_name(self, js_analyzer):
        """Test const foo = require('./module').foo."""
        code = "const helperFunc = require('./helper').helperFunc;"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./helper"
        # When var name equals property, no alias needed
        assert ("helperFunc", None) in imports[0].named_imports

    def test_require_external_package(self, js_analyzer):
        """Test require for external packages."""
        code = "const lodash = require('lodash');"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "lodash"
        assert imports[0].default_import == "lodash"

    def test_require_side_effect_import(self, js_analyzer):
        """Test require('./module') without assignment."""
        code = "require('./side-effects');"
        imports = js_analyzer.find_imports(code)

        assert len(imports) == 1
        assert imports[0].module_path == "./side-effects"
        assert imports[0].default_import is None
        assert imports[0].named_imports == []


class TestCommonJSExports:
    """Tests for CommonJS module.exports parsing."""

    @pytest.fixture
    def js_analyzer(self):
        """Create a JavaScript analyzer."""
        from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage

        return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)

    @pytest.fixture
    def ts_analyzer(self):
        """Create a TypeScript analyzer."""
        from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage

        return TreeSitterAnalyzer(TreeSitterLanguage.TYPESCRIPT)

    def test_module_exports_function(self, js_analyzer):
        """Test module.exports = function() {}."""
        code = "module.exports = function helper() { return 1; };"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].default_export == "helper"

    def test_module_exports_anonymous_function(self, js_analyzer):
        """Test module.exports = function() {} (anonymous)."""
        code = "module.exports = function() { return 1; };"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].default_export == "default"

    def test_module_exports_arrow_function(self, js_analyzer):
        """Test module.exports = () => {}."""
        code = "module.exports = () => { return 1; };"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].default_export == "default"

    def test_module_exports_identifier(self, js_analyzer):
        """Test module.exports = existingFunction."""
        code = """
function helper() { return 1; }
module.exports = helper;
"""
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].default_export == "helper"

    def test_module_exports_object(self, js_analyzer):
        """Test module.exports = { foo, bar }."""
        code = """
function foo() {}
function bar() {}
module.exports = { foo, bar };
"""
        exports = js_analyzer.find_exports(code)

        # Should find the module.exports object
        module_export = [e for e in exports if e.exported_names]
        assert len(module_export) == 1
        assert ("foo", None) in module_export[0].exported_names
        assert ("bar", None) in module_export[0].exported_names

    def test_module_exports_object_with_rename(self, js_analyzer):
        """Test module.exports = { publicName: localFunc }."""
        code = """
function helper() {}
module.exports = { publicHelper: helper };
"""
        exports = js_analyzer.find_exports(code)

        module_export = [e for e in exports if e.exported_names]
        assert len(module_export) == 1
        # helper is exported as publicHelper
        assert ("helper", "publicHelper") in module_export[0].exported_names

    def test_module_exports_property(self, js_analyzer):
        """Test module.exports.foo = function() {}."""
        code = "module.exports.helper = function() { return 1; };"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("helper", None) in exports[0].exported_names

    def test_exports_property(self, js_analyzer):
        """Test exports.foo = function() {}."""
        code = "exports.helper = function() { return 1; };"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert ("helper", None) in exports[0].exported_names

    def test_module_exports_require_reexport(self, js_analyzer):
        """Test module.exports = require('./other')."""
        code = "module.exports = require('./other');"
        exports = js_analyzer.find_exports(code)

        assert len(exports) == 1
        assert exports[0].is_reexport is True
        assert exports[0].reexport_source == "./other"

    def test_is_function_exported_commonjs(self, js_analyzer):
        """Test is_function_exported works with CommonJS exports."""
        code = """
function helper() { return 1; }
module.exports = { helper };
"""
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is True
        assert export_name == "helper"

    def test_is_function_exported_commonjs_property(self, js_analyzer):
        """Test is_function_exported with exports.foo pattern."""
        code = """
function helper() { return 1; }
exports.helper = helper;
"""
        is_exported, export_name = js_analyzer.is_function_exported(code, "helper")

        assert is_exported is True
        assert export_name == "helper"

    def test_is_class_method_exported_via_class(self, ts_analyzer):
        """Test is_function_exported returns True for method of exported class."""
        code = """
export class BloomFilter {
    getHashValues(key: string): number[] {
        return [1, 2, 3];
    }
}
"""
        # Method itself is not directly exported
        is_exported, export_name = ts_analyzer.is_function_exported(code, "getHashValues")
        assert is_exported is False
        assert export_name is None

        # But when we pass the class name, it should find the class export
        is_exported, export_name = ts_analyzer.is_function_exported(code, "getHashValues", "BloomFilter")
        assert is_exported is True
        assert export_name == "BloomFilter"

    def test_is_class_method_exported_default_class(self, ts_analyzer):
        """Test is_function_exported returns True for method of default exported class."""
        code = """
export default class Calculator {
    add(a: number, b: number): number {
        return a + b;
    }
}
"""
        # When we pass the class name, it should find the default export
        is_exported, export_name = ts_analyzer.is_function_exported(code, "add", "Calculator")
        assert is_exported is True
        assert export_name == "Calculator"

    def test_is_class_method_not_exported_non_exported_class(self, ts_analyzer):
        """Test is_function_exported returns False for method of non-exported class."""
        code = """
class InternalClass {
    helper(): void {}
}
"""
        # Even with class name, non-exported class method should not be exported
        is_exported, export_name = ts_analyzer.is_function_exported(code, "helper", "InternalClass")
        assert is_exported is False
        assert export_name is None


class TestCommonJSImportResolver:
    """Tests for ImportResolver with CommonJS require() imports."""

    @pytest.fixture
    def project_root(self, tmp_path):
        """Create a temporary project structure with CommonJS files."""
        src_dir = tmp_path / "src"
        src_dir.mkdir()

        # Create CommonJS module files
        (src_dir / "main.js").write_text("""
const helper = require('./helper');
const { add, subtract } = require('./math');

function main() {
    return helper.process() + add(1, 2);
}

module.exports = main;
""")

        (src_dir / "helper.js").write_text("""
function process() {
    return 42;
}

module.exports = { process };
""")

        (src_dir / "math.js").write_text("""
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }

module.exports = { add, subtract };
""")

        return tmp_path

    @pytest.fixture
    def resolver(self, project_root):
        """Create an ImportResolver for the project."""
        return ImportResolver(project_root)

    def test_resolve_commonjs_default_require(self, resolver, project_root):
        """Test resolving const foo = require('./module')."""
        source_file = project_root / "src" / "main.js"
        import_info = ImportInfo(
            module_path="./helper",
            default_import="helper",
            named_imports=[],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "helper.js"

    def test_resolve_commonjs_destructured_require(self, resolver, project_root):
        """Test resolving const { a, b } = require('./module')."""
        source_file = project_root / "src" / "main.js"
        import_info = ImportInfo(
            module_path="./math",
            default_import=None,
            named_imports=[("add", None), ("subtract", None)],
            namespace_import=None,
            is_type_only=False,
            start_line=1,
            end_line=1,
        )

        result = resolver.resolve_import(import_info, source_file)

        assert result is not None
        assert result.file_path == project_root / "src" / "math.js"
        assert "add" in result.imported_names
        assert "subtract" in result.imported_names
