"""Tests for tree-sitter utilities from analysis/adapters/_treesitter_utils.py.

Tests traverse, extract_es_imports, extract_require_imports, extract_definitions,
extract_entry_points, and calculate_complexity functions.
"""

from __future__ import annotations

from unfamiliar.analysis.adapters._treesitter_utils import (
    calculate_complexity,
    get_node_text,
    traverse,
)
from unfamiliar.analysis.adapters._ts_definitions import extract_definitions
from unfamiliar.analysis.adapters._ts_entry_points import extract_entry_points
from unfamiliar.analysis.adapters._ts_imports import (
    _get_string_content,
    extract_es_imports,
    extract_require_imports,
)
from unfamiliar.constants import EntryType


class TestGetNodeText:
    """Tests for _get_node_text internal helper."""

    def test_null_node_returns_empty_string(self):
        """Passing None should return an empty string, not crash."""
        result = get_node_text(None)
        assert result == ""

    def test_valid_node_returns_text(self, js_parser):
        """Passing a valid node should return its text."""
        source = "const foo = 'bar';"
        root = js_parser.parse(source.encode()).root_node

        child = root.children[0]
        result = get_node_text(child)

        assert result == "const foo = 'bar';"


class TestGetStringContent:
    """Tests for _get_string_content internal helper."""

    def test_single_quotes(self, js_parser):
        """Single-quoted string returns content without quotes"""
        source = "'hello world'"
        root = js_parser.parse(source.encode()).root_node

        string_node = root.children[0]
        result = _get_string_content(string_node)

        assert result == "hello world"

    def test_double_quotes(self, js_parser):
        """Double-quoted string returns content without quotes"""
        source = '"hello world"'
        root = js_parser.parse(source.encode()).root_node

        string_node = root.children[0]
        result = _get_string_content(string_node)

        assert result == "hello world"

    def test_backtick_template(self, js_parser):
        """Template literal returns content without backticks"""

        source = "`hello world`"
        root = js_parser.parse(source.encode()).root_node

        string_node = root.children[0]
        result = _get_string_content(string_node)

        assert result == "hello world"

    def test_empty_string_returns_empty(self, js_parser):
        """Empty string literal returns empty content"""
        source = '""'
        root = js_parser.parse(source.encode()).root_node

        string_node = root.children[0]
        result = _get_string_content(string_node)

        assert result == ""

    def test_null_node_returns_none(self):
        """Null node returns None, not crash"""
        result = _get_string_content(None)
        assert result is None


class TestTraverse:
    """Tests for traverse function."""

    def test_no_duplicate_nodes(self, js_parser):
        """traverse yields each node position exactly once."""
        source = "const x = 1; const y = 2;"
        root = js_parser.parse(source.encode()).root_node

        # Identify nodes by (type, start_byte, end_byte) since tree-sitter
        # cursor-based traversal reuses wrapper objects (id() is unreliable).
        positions = [(n.type, n.start_byte, n.end_byte) for n in traverse(root)]

        # No duplicate positions
        assert len(positions) == len(set(positions))

    def test_root_node_first(self, js_parser):
        """traverse yields root node first."""
        source = "const x = 1;"
        root = js_parser.parse(source.encode()).root_node

        nodes = list(traverse(root))
        assert nodes[0] == root

    def test_single_node_tree(self, js_parser):
        """Empty source produces a program root with no children — exactly 1 node."""
        source = ""
        root = js_parser.parse(source.encode()).root_node

        nodes = list(traverse(root))
        assert len(nodes) == 1

    def test_no_recursion_error_on_deep_tree(self, js_parser):
        """Deeply nested tree doesn't cause RecursionError."""
        # Create deeply nested code
        source = "if (true) {\n"
        for _ in range(100):
            source += "  if (true) {\n"
        source += "    x = 1;\n"
        for _ in range(100):
            source += "  }\n"

        root = js_parser.parse(source.encode()).root_node

        # Should not raise RecursionError
        nodes = list(traverse(root))
        assert len(nodes) > 0


class TestExtractEsImports:
    """Tests for extract_es_imports function."""

    def test_simple_named_import(self, js_parser):
        """import foo from 'bar' extracts one import."""
        source = "import foo from 'bar';"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_es_imports(root, "test.js")

        assert len(imports) == 1
        imp = imports[0]
        assert imp.source_path == "test.js"
        assert imp.target == "bar"
        assert imp.is_relative is False
        assert imp.names == ["foo"]

    def test_named_imports_multiple(self, js_parser):
        """import { a, b } from 'mod' extracts two names."""
        source = "import { a, b } from 'mod';"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_es_imports(root, "test.js")

        assert len(imports) == 1
        assert set(imports[0].names) == {"a", "b"}

    def test_namespace_import(self, js_parser):
        """import * as ns from 'mod' extracts namespace."""
        source = "import * as ns from 'mod';"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_es_imports(root, "test.js")

        assert len(imports) == 1
        assert imports[0].names == ["*"]

    def test_relative_import_true(self, js_parser):
        """import './relative' has is_relative=True."""
        source = "import foo from './relative';"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_es_imports(root, "test.js")

        assert len(imports) == 1
        assert imports[0].is_relative is True
        assert imports[0].target == "./relative"

    def test_side_effect_import(self, js_parser):
        """import 'polyfill' side-effect import captured."""
        source = "import 'polyfill';"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_es_imports(root, "test.js")

        assert len(imports) == 1
        assert imports[0].names == []


class TestExtractRequireImports:
    """Tests for extract_require_imports function."""

    def test_simple_require(self, js_parser):
        """const x = require('./path') extracts import."""
        source = "const x = require('./path');"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_require_imports(root, "test.js")

        assert len(imports) == 1
        assert imports[0].is_relative is True
        assert imports[0].target == "./path"
        assert imports[0].source_path == "test.js"
        assert imports[0].names == []

    def test_external_require(self, js_parser):
        """require('express') is not relative."""
        source = "const y = require('express');"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_require_imports(root, "test.js")

        assert len(imports) == 1
        assert imports[0].is_relative is False
        assert imports[0].target == "express"
        assert imports[0].source_path == "test.js"
        assert imports[0].names == []

    def test_dynamic_require_not_captured(self, js_parser):
        """require(variable) is not captured."""
        source = "const z = require(variable);"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_require_imports(root, "test.js")

        assert len(imports) == 0

    def test_require_resolve_not_captured(self, js_parser):
        """require.resolve() is not captured."""
        source = "require.resolve('./mod');"
        root = js_parser.parse(source.encode()).root_node

        imports = extract_require_imports(root, "test.js")

        # require.resolve is a different call
        assert len(imports) == 0


class TestExtractDefinitions:
    """Tests for extract_definitions function."""

    def test_named_function(self, js_parser):
        """Named function declaration extracts FunctionDef."""
        source = "function foo() { return 1; }"
        root = js_parser.parse(source.encode()).root_node

        defs = extract_definitions(root)

        assert len(defs) == 1
        d = defs[0]
        assert d.name == "foo"
        assert d.kind == "function"
        assert d.is_async is False
        assert d.line_start == 1
        assert d.line_end == 1

    def test_arrow_function_variable(self, js_parser):
        """Arrow function assigned to variable extracts exactly 1 FunctionDef."""
        source = "const foo = () => 1;"
        root = js_parser.parse(source.encode()).root_node

        defs = extract_definitions(root)

        assert len(defs) == 1
        assert defs[0].name == "foo"
        assert defs[0].kind == "function"
        assert defs[0].line_start == 1
        assert defs[0].line_end == 1
        assert defs[0].is_async is False

    def test_class_with_methods(self, js_parser):
        """Class with 2 methods extracts exactly 3 FunctionDefs: 1 class + 2 methods."""
        source = """
class Foo {
    method1() { return 1; }
    method2() { return 2; }
}
"""
        root = js_parser.parse(source.encode()).root_node

        defs = extract_definitions(root)

        assert len(defs) == 3
        defs = sorted(defs, key=lambda d: d.line_start)

        # First definition: the class (lines 2-5)
        class_def = defs[0]
        assert class_def.name == "Foo"
        assert class_def.kind == "class"
        assert class_def.is_async is False
        assert class_def.line_start == 2
        assert class_def.line_end == 5

        #     Second definition: method1 (line 3)
        method1_def = defs[1]
        assert method1_def.name == "Foo.method1"
        assert method1_def.kind == "method"
        assert method1_def.is_async is False
        assert method1_def.line_start == 3
        assert method1_def.line_end == 3

        #    Third definition: method2 (line 4)
        method2 = defs[2]
        assert method2.name == "Foo.method2"
        assert method2.kind == "method"
        assert method2.is_async is False
        assert method2.line_start == 4
        assert method2.line_end == 4

    def test_async_function(self, js_parser):
        """async function has is_async=True and exactly 1 definition."""
        source = "async function foo() { return 1; }"
        root = js_parser.parse(source.encode()).root_node

        defs = extract_definitions(root)

        assert len(defs) == 1
        assert defs[0].name == "foo"
        assert defs[0].is_async is True
        assert defs[0].line_start == 1
        assert defs[0].line_end == 1
        assert defs[0].kind == "function"

    def test_no_definitions_empty_list(self, js_parser):
        """const assignment to a literal value returns no definitions."""
        source = "const x = 1;"
        root = js_parser.parse(source.encode()).root_node

        defs = extract_definitions(root)

        assert defs == []


class TestExtractEntryPoints:
    """Tests for extract_entry_points function."""

    def test_express_app_creation(self, js_parser):
        """const app = express() creates exactly 1 API_ROUTE entry point."""
        source = "const app = express();"
        root = js_parser.parse(source.encode()).root_node

        entries = extract_entry_points(root, "src/app.js")

        assert len(entries) == 1
        assert entries[0].entry_type == EntryType.API_ROUTE
        assert entries[0].file_path == "src/app.js"
        assert entries[0].description == "Express/Fastify app creation: app"
        assert entries[0].line == 1

    def test_app_listen(self, js_parser):
        """app.listen(3000) creates exactly 1 API_ROUTE entry point."""
        source = "app.listen(3000);"
        root = js_parser.parse(source.encode()).root_node

        entries = extract_entry_points(root, "src/app.js")

        assert len(entries) == 1
        assert entries[0].entry_type == EntryType.API_ROUTE
        assert entries[0].file_path == "src/app.js"
        assert entries[0].description == "app.listen() call"
        assert entries[0].line == 1

    def test_require_main_module_pattern(self, js_parser):
        """if (require.main === module) creates exactly 1 MAIN entry point."""
        source = "if (require.main === module) { console.log('main'); }"
        root = js_parser.parse(source.encode()).root_node

        entries = extract_entry_points(root, "src/app.js")

        assert len(entries) == 1
        assert entries[0].entry_type == EntryType.MAIN
        assert entries[0].file_path == "src/app.js"
        assert entries[0].description == "if (require.main === module) pattern"
        assert entries[0].line == 1

    def test_non_index_file_no_default_export_entry(self, js_parser):
        """Non-index file with default export has no entry points."""
        source = "export default { foo: 1 };"
        root = js_parser.parse(source.encode()).root_node

        entries = extract_entry_points(root, "src/component.js")

        assert entries == []

    def test_index_file_default_export_creates_main_entry(self, js_parser):
        """Default export in index.js creates MAIN entry point."""
        source = "export default { foo: 1 };"
        root = js_parser.parse(source.encode()).root_node

        entries = extract_entry_points(root, "src/index.js")

        assert len(entries) == 1
        assert entries[0].entry_type == EntryType.MAIN
        assert entries[0].file_path == "src/index.js"
        assert entries[0].description == "Default export in index file"
        assert entries[0].line == 1


class TestCalculateComplexity:
    """Tests for calculate_complexity function."""

    def test_empty_source_zero(self, js_parser):
        """Empty source has complexity 0."""
        source = ""
        root = js_parser.parse(source.encode()).root_node

        complexity = calculate_complexity(root)
        assert complexity == 0

    def test_single_if_one(self, js_parser):
        """Single if statement has complexity exactly 1."""
        source = "if (true) { console.log('yes'); }"
        root = js_parser.parse(source.encode()).root_node

        complexity = calculate_complexity(root)
        assert complexity == 1

    def test_if_for_while_sum(self, js_parser):
        """if + for + while each count 1, total complexity is exactly 3."""
        source = """
if (true) { x = 1; }
for (let i = 0; i < 10; i++) { x++; }
while (true) { break; }
"""
        root = js_parser.parse(source.encode()).root_node

        complexity = calculate_complexity(root)
        assert complexity == 3

    def test_and_or_counted(self, js_parser):
        """&& and || each count 1; combined with if: complexity is exactly 3."""
        source = "if (a && b || c) { x = 1; }"
        root = js_parser.parse(source.encode()).root_node

        # if_statement (+1) + binary_expression && (+1) + binary_expression || (+1)
        complexity = calculate_complexity(root)
        assert complexity == 3

    def test_only_type_declarations_zero(self, js_parser):
        """Source with only type-like declarations has complexity 0."""
        source = "const Foo = {};"
        root = js_parser.parse(source.encode()).root_node

        complexity = calculate_complexity(root)
        assert complexity == 0
