"""Tests for JavaScript test generation module.

Tests the prompt building and validation functions without importing
the full testgen module (to avoid LLM dependencies in tests).
"""

import re
from pathlib import Path

import pytest

# Load prompts directly to avoid importing testgen_javascript.py
current_dir = Path(__file__).parent.parent.parent / "core" / "languages" / "python" / "testgen"
JS_PROMPTS_DIR = current_dir / "prompts" / "javascript"

JS_EXECUTE_SYSTEM_PROMPT = (JS_PROMPTS_DIR / "execute_system_prompt.md").read_text()
JS_EXECUTE_USER_PROMPT = (JS_PROMPTS_DIR / "execute_user_prompt.md").read_text()
JS_EXECUTE_ASYNC_SYSTEM_PROMPT = (JS_PROMPTS_DIR / "execute_async_system_prompt.md").read_text()
JS_EXECUTE_ASYNC_USER_PROMPT = (JS_PROMPTS_DIR / "execute_async_user_prompt.md").read_text()

JS_PATTERN = re.compile(r"^```(?:javascript|js|typescript|ts)?\s*\n(.*?)\n```", re.MULTILINE | re.DOTALL)


def _has_test_functions(code: str) -> bool:
    """Check if the code contains Jest test functions."""
    test_pattern = r"(?:test|it)\s*\(\s*['\"]"
    return bool(re.search(test_pattern, code))


def build_javascript_prompt(
    function_name: str, function_code: str, module_path: str, test_framework: str, is_async: bool
) -> tuple[list[dict[str, str]], str]:
    """Build the prompt messages for JavaScript test generation."""
    if is_async:
        system_prompt = JS_EXECUTE_ASYNC_SYSTEM_PROMPT
        user_prompt = JS_EXECUTE_ASYNC_USER_PROMPT
        posthog_event_suffix = "async-"
    else:
        system_prompt = JS_EXECUTE_SYSTEM_PROMPT
        user_prompt = JS_EXECUTE_USER_PROMPT
        posthog_event_suffix = ""

    system_message = {"role": "system", "content": system_prompt.format(function_name=function_name)}

    user_message = {
        "role": "user",
        "content": user_prompt.format(
            test_framework=test_framework,
            function_name=function_name,
            function_code=function_code,
            module_path=module_path,
            package_comment="",
        ),
    }

    messages = [system_message, user_message]
    return messages, posthog_event_suffix


def parse_and_validate_js_output(response_content: str) -> str:
    """Parse and validate the LLM response for JavaScript code."""
    if "```" not in response_content:
        raise ValueError("LLM response did not contain a code block.")

    pattern_res = JS_PATTERN.search(response_content)
    if not pattern_res:
        raise ValueError("No JavaScript code block found in the LLM response.")

    code = pattern_res.group(1).strip()

    if not _has_test_functions(code):
        raise ValueError("Generated code does not contain any test functions.")

    return code


class TestHasTestFunctions:
    """Tests for detecting Jest test functions in code."""

    def test_detects_test_function(self) -> None:
        """Test that test() calls are detected."""
        code = """
test('should add numbers', () => {
    expect(add(1, 2)).toBe(3);
});
"""
        assert _has_test_functions(code) is True

    def test_detects_it_function(self) -> None:
        """Test that it() calls are detected."""
        code = """
it('should add numbers', () => {
    expect(add(1, 2)).toBe(3);
});
"""
        assert _has_test_functions(code) is True

    def test_detects_test_in_describe(self) -> None:
        """Test that tests inside describe blocks are detected."""
        code = """
describe('Calculator', () => {
    test('should add', () => {
        expect(add(1, 2)).toBe(3);
    });
});
"""
        assert _has_test_functions(code) is True

    def test_no_tests_returns_false(self) -> None:
        """Test that code without tests returns false."""
        code = """
function add(a, b) {
    return a + b;
}
"""
        assert _has_test_functions(code) is False

    def test_empty_code_returns_false(self) -> None:
        """Test that empty code returns false."""
        assert _has_test_functions("") is False

    def test_describe_only_returns_false(self) -> None:
        """Test that describe without tests returns false."""
        code = """
describe('Calculator', () => {
    // No tests yet
});
"""
        assert _has_test_functions(code) is False


class TestParseAndValidateJsOutput:
    """Tests for parsing and validating LLM output."""

    def test_extracts_javascript_code(self) -> None:
        """Test extracting code from JavaScript code block."""
        response = """Here are the tests:

```javascript
test('should work', () => {
    expect(true).toBe(true);
});
```
"""
        code = parse_and_validate_js_output(response)

        assert "test('should work'" in code
        assert "expect(true)" in code

    def test_extracts_js_code(self) -> None:
        """Test extracting code from js code block."""
        response = """```js
test('basic', () => {
    expect(1).toBe(1);
});
```"""
        code = parse_and_validate_js_output(response)

        assert "test('basic'" in code

    def test_raises_on_no_code_block(self) -> None:
        """Test that missing code block raises ValueError."""
        response = "This response has no code block."

        with pytest.raises(ValueError) as exc_info:
            parse_and_validate_js_output(response)

        assert "code block" in str(exc_info.value).lower()

    def test_raises_on_no_tests(self) -> None:
        """Test that code without tests raises ValueError."""
        response = """```javascript
function add(a, b) {
    return a + b;
}
```"""

        with pytest.raises(ValueError) as exc_info:
            parse_and_validate_js_output(response)

        assert "test" in str(exc_info.value).lower()


class TestBuildJavaScriptPrompt:
    """Tests for building JavaScript prompts."""

    def test_sync_function_prompt(self) -> None:
        """Test building prompt for sync function."""
        messages, suffix = build_javascript_prompt(
            function_name="fibonacci",
            function_code="function fibonacci(n) { return n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2); }",
            module_path="./fibonacci",
            test_framework="jest",
            is_async=False,
        )

        assert len(messages) == 2
        assert messages[0]["role"] == "system"
        assert messages[1]["role"] == "user"
        assert "fibonacci" in messages[0]["content"]
        assert "fibonacci" in messages[1]["content"]
        assert suffix == ""  # Non-async should have empty suffix

    def test_async_function_prompt(self) -> None:
        """Test building prompt for async function."""
        messages, suffix = build_javascript_prompt(
            function_name="fetchData",
            function_code="async function fetchData(url) { return await fetch(url); }",
            module_path="./api",
            test_framework="jest",
            is_async=True,
        )

        assert len(messages) == 2
        assert "async" in messages[0]["content"].lower()
        assert suffix == "async-"

    def test_prompt_includes_function_code(self) -> None:
        """Test that prompt includes the function source code."""
        function_code = "function add(a, b) { return a + b; }"
        messages, _ = build_javascript_prompt(
            function_name="add",
            function_code=function_code,
            module_path="./math",
            test_framework="jest",
            is_async=False,
        )

        # User message should contain the function code
        assert function_code in messages[1]["content"]

    def test_prompt_includes_module_path(self) -> None:
        """Test that prompt includes the module path."""
        messages, _ = build_javascript_prompt(
            function_name="add",
            function_code="function add(a, b) { return a + b; }",
            module_path="./math/utils",
            test_framework="jest",
            is_async=False,
        )

        assert "./math/utils" in messages[1]["content"]


class TestJavaScriptTestGenPromptContent:
    """Tests for JavaScript test generation prompt content."""

    def test_system_prompt_mentions_jest(self) -> None:
        """Test that system prompt mentions Jest framework."""
        messages, _ = build_javascript_prompt(
            function_name="test",
            function_code="function test() {}",
            module_path="./test",
            test_framework="jest",
            is_async=False,
        )

        assert "Jest" in messages[0]["content"] or "jest" in messages[0]["content"].lower()

    def test_system_prompt_has_test_categories(self) -> None:
        """Test that system prompt mentions test categories."""
        messages, _ = build_javascript_prompt(
            function_name="test",
            function_code="function test() {}",
            module_path="./test",
            test_framework="jest",
            is_async=False,
        )

        system_content = messages[0]["content"]
        # Should mention different test categories
        assert "Basic" in system_content or "basic" in system_content.lower()
        assert "Edge" in system_content or "edge" in system_content.lower()

    def test_system_prompt_warns_against_mocking(self) -> None:
        """Test that system prompt warns against mocking the function under test."""
        messages, _ = build_javascript_prompt(
            function_name="test",
            function_code="function test() {}",
            module_path="./test",
            test_framework="jest",
            is_async=False,
        )

        system_content = messages[0]["content"]
        # Should warn against mocking
        assert "mock" in system_content.lower() or "Mock" in system_content


class TestStripJsExtensions:
    """Tests for stripping file extensions from import paths.

    These tests copy the regex patterns and function directly to avoid Django dependencies.
    """

    # Copy of patterns from aiservice/languages/js_ts/testgen.py
    _JS_EXTENSION_PATTERN = re.compile(r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""")
    _REQUIRE_EXTENSION_PATTERN = re.compile(
        r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))"""
    )
    _JEST_MOCK_EXTENSION_PATTERN = re.compile(
        r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
    )

    @staticmethod
    def strip_js_extensions(source: str) -> str:
        """Strip .js/.ts/.tsx/.jsx extensions from relative import paths."""
        source = TestStripJsExtensions._JS_EXTENSION_PATTERN.sub(r"\1\2\4", source)
        source = TestStripJsExtensions._REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source)
        return TestStripJsExtensions._JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)

    def test_strip_js_extension_from_esm_import(self) -> None:
        """Test stripping .js from ES module imports."""
        code = "import { getDifferences } from '../src/utils/DynamicBindingUtils.js';"
        expected = "import { getDifferences } from '../src/utils/DynamicBindingUtils';"

        result = self.strip_js_extensions(code)
        assert result == expected

    def test_strip_ts_extension_from_esm_import(self) -> None:
        """Test stripping .ts from ES module imports."""
        code = "import { func } from './module.ts';"
        expected = "import { func } from './module';"

        result = self.strip_js_extensions(code)
        assert result == expected

    def test_strip_extension_from_require(self) -> None:
        """Test stripping extensions from require() calls."""
        code = "const { func } = require('../utils/helper.js');"
        expected = "const { func } = require('../utils/helper');"

        result = self.strip_js_extensions(code)
        assert result == expected

    def test_strip_extension_from_jest_mock(self) -> None:
        """Test stripping extensions from jest.mock() calls."""
        code = "jest.mock('../src/utils/DynamicBindingUtils.js');"
        expected = "jest.mock('../src/utils/DynamicBindingUtils');"

        result = self.strip_js_extensions(code)
        assert result == expected

    def test_preserve_external_package_imports(self) -> None:
        """Test that external package imports are not modified."""
        code = "import lodash from 'lodash';"

        result = self.strip_js_extensions(code)
        assert result == code  # Should be unchanged

    def test_strip_multiple_extensions_in_file(self) -> None:
        """Test stripping multiple extensions in a single file."""
        code = """
import { func1 } from '../utils/helper.js';
import { func2 } from './local.ts';
const { func3 } = require('../lib/util.tsx');
jest.mock('../mocks/mock.jsx');
"""
        expected = """
import { func1 } from '../utils/helper';
import { func2 } from './local';
const { func3 } = require('../lib/util');
jest.mock('../mocks/mock');
"""

        result = self.strip_js_extensions(code)
        assert result == expected
