"""Tests for JavaScript test generation module.

Tests the prompt building and validation functions.
"""

import re

import pytest

from core.languages.js_ts.testgen import build_javascript_prompt, parse_and_validate_js_output, strip_js_extensions


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))


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"
        system = messages[0]["content"]
        user = messages[1]["content"]
        assert isinstance(system, str)
        assert isinstance(user, str)
        assert "fibonacci" in system
        assert "fibonacci" in user
        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
        system = messages[0]["content"]
        assert isinstance(system, str)
        assert "async" in system.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
        user = messages[1]["content"]
        assert isinstance(user, str)
        assert function_code in user

    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,
        )

        user = messages[1]["content"]
        assert isinstance(user, str)
        assert "./math/utils" in user


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,
        )

        system = messages[0]["content"]
        assert isinstance(system, str)
        assert "Jest" in system or "jest" in system.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"]
        assert isinstance(system_content, str)
        # 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"]
        assert isinstance(system_content, str)
        # Should warn against mocking
        assert "mock" in system_content.lower() or "Mock" in system_content


class TestMochaPromptContent:
    """Tests for Mocha-specific prompt content."""

    def test_mocha_system_prompt_uses_assert(self) -> None:
        """Test that Mocha system prompt mentions node:assert and not expect()."""
        messages, _ = 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="mocha",
            is_async=False,
        )
        system_content = messages[0]["content"]
        assert isinstance(system_content, str)
        assert "node:assert" in system_content
        assert "strictEqual" in system_content

    def test_mocha_user_prompt_uses_it(self) -> None:
        """Test that Mocha user prompt template uses it() not test()."""
        messages, _ = 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="mocha",
            is_async=False,
        )
        user_content = messages[1]["content"]
        assert isinstance(user_content, str)
        assert "it(" in user_content
        assert "assert.strictEqual" in user_content

    def test_mocha_prompt_no_other_framework_apis(self) -> None:
        """Test that Mocha prompts don't contain vitest/jest/chai API references."""
        messages, _ = 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="mocha",
            is_async=False,
        )
        system_content = messages[0]["content"]
        user_content = messages[1]["content"]
        assert isinstance(system_content, str)
        assert isinstance(user_content, str)
        # Prompts should not contain vitest/jest/chai imports or API references
        assert "from 'vitest'" not in system_content
        assert "from 'vitest'" not in user_content
        assert "jest.mock(" not in system_content
        assert "jest.mock(" not in user_content
        assert "vi.mock(" not in system_content
        assert "vi.mock(" not in user_content
        assert "from 'chai'" not in system_content
        assert "from 'chai'" not in user_content
        # Should not mention expect().toBe() patterns
        assert "toBe(" not in user_content
        assert "toEqual(" not in user_content

    def test_mocha_async_prompt_uses_assert_rejects(self) -> None:
        """Test that async Mocha prompts use assert.rejects instead of expect().rejects."""
        messages, _ = build_javascript_prompt(
            function_name="fetchData",
            function_code="async function fetchData(url) { return await fetch(url); }",
            module_path="./api",
            test_framework="mocha",
            is_async=True,
        )
        system_content = messages[0]["content"]
        user_content = messages[1]["content"]
        assert isinstance(system_content, str)
        assert isinstance(user_content, str)
        assert "assert.rejects" in system_content
        assert "assert.rejects" in user_content

    def test_jest_prompt_unchanged(self) -> None:
        """Regression: Jest prompts should still use expect/test syntax."""
        messages, _ = build_javascript_prompt(
            function_name="add",
            function_code="function add(a, b) { return a + b; }",
            module_path="./math",
            test_framework="jest",
            is_async=False,
        )
        user_content = messages[1]["content"]
        assert isinstance(user_content, str)
        assert "expect" in user_content.lower()
        assert "test(" in user_content or "it(" in user_content

    def test_vitest_prompt_unchanged(self) -> None:
        """Regression: Vitest prompts should still use expect/test syntax and vitest imports."""
        messages, _ = build_javascript_prompt(
            function_name="add",
            function_code="function add(a, b) { return a + b; }",
            module_path="./math",
            test_framework="vitest",
            is_async=False,
        )
        user_content = messages[1]["content"]
        assert isinstance(user_content, str)
        assert "vitest" in user_content.lower()
        assert "expect" in user_content.lower()


class TestStripJsExtensions:
    """Tests for stripping .js/.ts extensions from import paths."""

    def test_strips_extensions_from_imports(self) -> None:
        """Test that extensions are stripped from ES module imports."""
        source = "import { x } from '../path/file.js';"
        result = strip_js_extensions(source)
        assert result == "import { x } from '../path/file';"

    def test_strips_extensions_from_require(self) -> None:
        """Test that extensions are stripped from require() calls."""
        source = "const x = require('../path/file.js');"
        result = strip_js_extensions(source)
        assert result == "const x = require('../path/file');"

    def test_strips_extensions_from_jest_mock(self) -> None:
        """Test that extensions are stripped from jest.mock() calls."""
        source = "jest.mock('../path/file.js', () => {});"
        result = strip_js_extensions(source)
        assert result == "jest.mock('../path/file', () => {});"

    def test_strips_extensions_from_vi_mock(self) -> None:
        """Test that extensions are stripped from vi.mock() calls (Vitest).

        This is a regression test for the bug where vi.mock() paths retained
        .js extensions while imports had them stripped, causing mock/import
        path mismatch in Vitest ESM mode.

        Trace IDs affected:
        - 0fe99c9f-b348-4f0a-b051-0ea9455231ba
        - 127cdaec-a343-4918-a86a-b646dd4d79cf
        - 2b6c896e-20d7-4505-8bf4-e4a2f20b37fc
        """
        source = "vi.mock('../config/paths.js', () => {});"
        result = strip_js_extensions(source)
        # This test will FAIL until the bug is fixed
        assert result == "vi.mock('../config/paths', () => {});"

    def test_strips_extensions_from_complex_vi_mock(self) -> None:
        """Test extension stripping for complex vi.mock() with multiline callback."""
        source = """vi.mock('../config/paths.js', () => {
  return {
    resolveCredentialsDir: vi.fn(() => '/mock/credentials'),
  };
});"""
        result = strip_js_extensions(source)
        assert "vi.mock('../config/paths'" in result
        assert "vi.mock('../config/paths.js'" not in result

    def test_strips_all_vi_mock_variants(self) -> None:
        """Test that all vi.mock variants are handled."""
        source = """
vi.mock('../a.js', () => {});
vi.doMock('../b.js', () => {});
vi.unmock('../c.js');
"""
        result = strip_js_extensions(source)
        assert "../a'" in result
        assert "../b'" in result
        assert "../c'" in result
        assert ".js" not in result

    def test_preserves_node_modules_paths(self) -> None:
        """Test that node_modules paths (without ./) are not modified."""
        source = "import { x } from 'some-package';"
        result = strip_js_extensions(source)
        assert result == source

    def test_handles_mixed_mocks_and_imports(self) -> None:
        """Test realistic scenario with both vi.mock() and imports."""
        source = """vi.mock('../config/paths.js', () => {
  return {
    resolveCredentialsDir: vi.fn(() => '/mock/credentials'),
  };
});

import { resolveChannelAllowFromPath } from './pairing/pairing-store.js';
import { resolveCredentialsDir } from '../config/paths.js';"""

        result = strip_js_extensions(source)

        # All .js extensions should be removed
        assert "vi.mock('../config/paths'" in result
        assert "from './pairing/pairing-store'" in result
        assert "from '../config/paths'" in result

        # No .js should remain
        assert ".js" not in result


class TestInstrumentedTestsExtensionStripping:
    """Tests for ensuring .js extensions are stripped from ALL test outputs."""

    def test_strip_extensions_on_all_outputs(self) -> None:
        """Test that .js extensions should be stripped from instrumented tests too.

        This is a regression test for the bug where strip_js_extensions() was only
        called on generated_test_source but not on instrumented_behavior_tests
        and instrumented_perf_tests, causing "Cannot find module" errors in the CLI.
        """
        # Simulated LLM output with .js extensions (what comes back from LLM)
        llm_generated_test = """import { buildVerifyFn } from '../../google.js';
import { authenticate } from '../../sso.js';

test('should create verify function', () => {
  const fn = buildVerifyFn(mockSave);
  expect(fn).toBeDefined();
});"""

        # All three test outputs should have extensions stripped
        # (in practice, instrumented tests have capture() calls added, but for this test we're checking extension stripping)
        expected_stripped = """import { buildVerifyFn } from '../../google';
import { authenticate } from '../../sso';

test('should create verify function', () => {
  const fn = buildVerifyFn(mockSave);
  expect(fn).toBeDefined();
});"""

        # Verify that strip_js_extensions works
        result = strip_js_extensions(llm_generated_test)
        assert result == expected_stripped, "strip_js_extensions should remove .js extensions"

        # Regression test: verifies strip_js_extensions() is applied correctly.
        # For full end-to-end coverage, an integration test calling testgen_javascript()
        # and asserting all three return values would be ideal.
