"""End-to-end tests for JavaScript/TypeScript test execution and result parsing.

These tests verify the FULL optimization pipeline including:
- Test instrumentation
- Running instrumented tests with Vitest/Jest
- Parsing test results (stdout, timing, return values)
- Benchmarking with multiple loops

This is the JavaScript equivalent of test_instrument_tests.py for Python.

NOTE: These tests require:
- Node.js installed
- npm packages installed in the test fixture directories
- The codeflash npm package

Tests will be skipped if dependencies are not available.
"""

import subprocess
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from codeflash.discovery.functions_to_optimize import FunctionToOptimize
from codeflash.languages.base import Language
from codeflash.models.models import FunctionParent
from codeflash.verification.verification_utils import TestConfig


def is_node_available():
    """Check if Node.js is available."""
    try:
        result = subprocess.run(["node", "--version"], capture_output=True, text=True)
        return result.returncode == 0
    except FileNotFoundError:
        return False


def is_npm_available():
    """Check if npm is available."""
    try:
        result = subprocess.run(["npm", "--version"], capture_output=True, text=True)
        return result.returncode == 0
    except FileNotFoundError:
        return False


def has_node_modules(project_dir: Path) -> bool:
    """Check if node_modules exists in project directory."""
    return (project_dir / "node_modules").exists()


def install_dependencies(project_dir: Path) -> bool:
    """Install npm dependencies in project directory."""
    if has_node_modules(project_dir):
        return True
    try:
        result = subprocess.run(["npm", "install"], cwd=project_dir, capture_output=True, text=True, timeout=120)
        return result.returncode == 0
    except Exception:
        return False


def skip_if_js_runtime_not_available():
    """Skip test if JavaScript runtime is not available."""
    if not is_node_available():
        pytest.skip("Node.js not available")
    if not is_npm_available():
        pytest.skip("npm not available")


def skip_if_js_not_supported():
    """Skip test if JavaScript/TypeScript languages are not supported."""
    try:
        from codeflash.languages import get_language_support

        get_language_support(Language.JAVASCRIPT)
    except Exception as e:
        pytest.skip(f"JavaScript/TypeScript language support not available: {e}")


class TestJavaScriptInstrumentation:
    """Tests for JavaScript test instrumentation."""

    @pytest.fixture
    def js_project_dir(self, tmp_path):
        """Create a temporary JavaScript project with Jest."""
        project_dir = tmp_path / "js_project"
        project_dir.mkdir()

        # Create source file
        src_file = project_dir / "math.js"
        src_file.write_text("""
function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

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

        # Create test file
        tests_dir = project_dir / "__tests__"
        tests_dir.mkdir()
        test_file = tests_dir / "math.test.js"
        test_file.write_text("""
const { add, multiply } = require('../math');

describe('math functions', () => {
    test('add returns sum', () => {
        expect(add(2, 3)).toBe(5);
    });

    test('multiply returns product', () => {
        expect(multiply(2, 3)).toBe(6);
    });
});
""")

        # Create package.json
        package_json = project_dir / "package.json"
        package_json.write_text("""{
    "name": "test-project",
    "version": "1.0.0",
    "scripts": {
        "test": "jest"
    },
    "devDependencies": {
        "jest": "^29.0.0",
        "jest-junit": "^16.0.0"
    }
}""")

        # Create jest.config.js
        jest_config = project_dir / "jest.config.js"
        jest_config.write_text("""
module.exports = {
    testEnvironment: 'node',
    reporters: ['default', 'jest-junit'],
};
""")

        return project_dir

    def test_instrument_javascript_test_file(self, js_project_dir):
        """Test that JavaScript test instrumentation module can be imported."""
        skip_if_js_not_supported()
        from codeflash.languages import get_language_support

        # Verify the instrumentation module can be imported

        # Get JavaScript support
        js_support = get_language_support(Language.JAVASCRIPT)

        # Create function info
        func_info = FunctionToOptimize(
            function_name="add",
            file_path=js_project_dir / "math.js",
            parents=[],
            starting_line=2,
            ending_line=4,
            language="javascript",
        )

        # Verify function has correct language
        assert func_info.language == "javascript"

        # Verify test file exists
        test_file = js_project_dir / "__tests__" / "math.test.js"
        assert test_file.exists()

        # Note: Full instrumentation test requires call_positions discovery
        # which is done by the FunctionOptimizer. Here we just verify the
        # infrastructure is in place.


class TestTypeScriptInstrumentation:
    """Tests for TypeScript test instrumentation."""

    @pytest.fixture
    def ts_project_dir(self, tmp_path):
        """Create a temporary TypeScript project with Vitest."""
        project_dir = tmp_path / "ts_project"
        project_dir.mkdir()

        # Create source file
        src_file = project_dir / "math.ts"
        src_file.write_text("""
export function add(a: number, b: number): number {
    return a + b;
}

export function multiply(a: number, b: number): number {
    return a * b;
}
""")

        # Create test file
        tests_dir = project_dir / "tests"
        tests_dir.mkdir()
        test_file = tests_dir / "math.test.ts"
        test_file.write_text("""
import { describe, test, expect } from 'vitest';
import { add, multiply } from '../math';

describe('math functions', () => {
    test('add returns sum', () => {
        expect(add(2, 3)).toBe(5);
    });

    test('multiply returns product', () => {
        expect(multiply(2, 3)).toBe(6);
    });
});
""")

        # Create package.json
        package_json = project_dir / "package.json"
        package_json.write_text("""{
    "name": "test-project",
    "version": "1.0.0",
    "type": "module",
    "scripts": {
        "test": "vitest run"
    },
    "devDependencies": {
        "vitest": "^1.0.0",
        "typescript": "^5.0.0"
    }
}""")

        # Create vitest.config.ts
        vitest_config = project_dir / "vitest.config.ts"
        vitest_config.write_text("""
import { defineConfig } from 'vitest/config';

export default defineConfig({
    test: {
        globals: false,
        reporters: ['verbose', 'junit'],
        outputFile: './junit.xml',
    },
});
""")

        # Create tsconfig.json
        tsconfig = project_dir / "tsconfig.json"
        tsconfig.write_text("""{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "node",
        "strict": true,
        "esModuleInterop": true
    }
}""")

        return project_dir

    def test_instrument_typescript_test_file(self, ts_project_dir):
        """Test that TypeScript test instrumentation module can be imported."""
        skip_if_js_not_supported()
        from codeflash.languages import get_language_support

        # Verify the instrumentation module can be imported

        test_file = ts_project_dir / "tests" / "math.test.ts"

        # Get TypeScript support
        ts_support = get_language_support(Language.TYPESCRIPT)

        # Create function info
        func_info = FunctionToOptimize(
            function_name="add",
            file_path=ts_project_dir / "math.ts",
            parents=[],
            starting_line=2,
            ending_line=4,
            language="typescript",
        )

        # Verify function has correct language
        assert func_info.language == "typescript"

        # Verify test file exists
        assert test_file.exists()

        # Note: Full instrumentation test requires call_positions discovery
        # which is done by the FunctionOptimizer. Here we just verify the
        # infrastructure is in place.


class TestRunAndParseJavaScriptTests:
    """Tests for running and parsing JavaScript test results.

    These tests require actual npm dependencies to be installed.
    They will be skipped if dependencies are not available.
    """

    @pytest.fixture
    def vitest_project(self):
        """Get the Vitest sample project with dependencies installed."""
        project_root = Path(__file__).parent.parent.parent
        vitest_dir = project_root / "code_to_optimize" / "js" / "code_to_optimize_vitest"

        if not vitest_dir.exists():
            pytest.skip("code_to_optimize_vitest directory not found")

        skip_if_js_runtime_not_available()

        # Try to install dependencies if not present
        if not has_node_modules(vitest_dir):
            if not install_dependencies(vitest_dir):
                pytest.skip("Could not install npm dependencies")

        return vitest_dir

    def test_run_behavioral_tests_vitest(self, vitest_project):
        """Test running behavioral tests with Vitest."""
        skip_if_js_not_supported()
        from codeflash.discovery.functions_to_optimize import find_all_functions_in_file
        from codeflash.languages import get_language_support

        ts_support = get_language_support(Language.TYPESCRIPT)

        # Find the fibonacci function
        fib_file = vitest_project / "fibonacci.ts"
        functions = find_all_functions_in_file(fib_file)
        fib_func = next(f for f in functions[fib_file] if f.function_name == "fibonacci")

        # Verify language is correct
        assert fib_func.language == "typescript"

        # Discover tests
        test_root = vitest_project / "tests"
        tests = ts_support.discover_tests(test_root, [fib_func])

        # There should be tests for fibonacci
        assert len(tests) > 0 or fib_func.qualified_name in tests

    def test_function_optimizer_run_and_parse_typescript(self, vitest_project):
        """Test FunctionOptimizer.run_and_parse_tests for TypeScript.

        This is the JavaScript equivalent of the Python test in test_instrument_tests.py.
        """
        skip_if_js_not_supported()
        from codeflash.discovery.functions_to_optimize import find_all_functions_in_file
        from codeflash.languages.javascript.function_optimizer import JavaScriptFunctionOptimizer

        # Find the fibonacci function
        fib_file = vitest_project / "fibonacci.ts"
        functions = find_all_functions_in_file(fib_file)
        fib_func_info = next(f for f in functions[fib_file] if f.function_name == "fibonacci")

        # Create FunctionToOptimize
        func = FunctionToOptimize(
            function_name=fib_func_info.function_name,
            file_path=fib_func_info.file_path,
            parents=[FunctionParent(name=p.name, type=p.type) for p in fib_func_info.parents],
            starting_line=fib_func_info.starting_line,
            ending_line=fib_func_info.ending_line,
            language=fib_func_info.language,
        )

        # Verify language
        assert func.language == "typescript"

        # Create test config
        test_config = TestConfig(
            tests_root=vitest_project / "tests",
            tests_project_rootdir=vitest_project,
            project_root_path=vitest_project,
            pytest_cmd="vitest",
            test_framework="vitest",
        )

        # Create optimizer
        func_optimizer = JavaScriptFunctionOptimizer(
            function_to_optimize=func, test_cfg=test_config, aiservice_client=MagicMock()
        )

        # Get code context - this should work
        result = func_optimizer.get_code_optimization_context()
        context = result.unwrap()

        assert context is not None
        assert context.read_writable_code.language == "typescript"


class TestTimingMarkerParsing:
    """Tests for parsing JavaScript timing markers from test output.

    Note: Timing marker parsing is handled in codeflash/verification/parse_test_output.py,
    which uses a unified parser for all languages. These tests verify the marker format
    is correctly recognized.
    """

    def test_timing_marker_format(self):
        """Test that JavaScript timing markers follow the expected format."""
        skip_if_js_not_supported()
        import re

        # The marker format used by codeflash for JavaScript
        # Start marker: !$######{tag}######$!
        # End marker: !######{tag}:{duration}######!
        start_pattern = r"!\$######(.+?)######\$!"
        end_pattern = r"!######(.+?):(\d+)######!"

        start_marker = "!$######test/math.test.ts:TestMath.test_add:add:1:0_0######$!"
        end_marker = "!######test/math.test.ts:TestMath.test_add:add:1:0_0:12345######!"

        start_match = re.match(start_pattern, start_marker)
        end_match = re.match(end_pattern, end_marker)

        assert start_match is not None
        assert end_match is not None
        assert start_match.group(1) == "test/math.test.ts:TestMath.test_add:add:1:0_0"
        assert end_match.group(1) == "test/math.test.ts:TestMath.test_add:add:1:0_0"
        assert end_match.group(2) == "12345"

    def test_timing_marker_components(self):
        """Test parsing components from timing marker tag."""
        skip_if_js_not_supported()

        # Tag format: {module}:{class}.{test}:{function}:{loop_index}:{invocation_id}
        tag = "test/math.test.ts:TestMath.test_add:add:1:0_0"
        parts = tag.split(":")

        assert len(parts) == 5
        assert parts[0] == "test/math.test.ts"  # module/file
        assert parts[1] == "TestMath.test_add"  # class.test
        assert parts[2] == "add"  # function being tested
        assert parts[3] == "1"  # loop index
        assert parts[4] == "0_0"  # invocation id


class TestJavaScriptTestResultParsing:
    """Tests for parsing JavaScript test results from JUnit XML."""

    def test_parse_vitest_junit_xml(self, tmp_path):
        """Test parsing Vitest JUnit XML output."""
        skip_if_js_not_supported()

        # Create sample JUnit XML
        junit_xml = tmp_path / "junit.xml"
        junit_xml.write_text("""<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="vitest tests" tests="2" failures="0" errors="0" time="0.5">
  <testsuite name="tests/math.test.ts" tests="2" failures="0" errors="0" time="0.5">
    <testcase classname="tests/math.test.ts" name="add returns sum" time="0.1">
    </testcase>
    <testcase classname="tests/math.test.ts" name="multiply returns product" time="0.2">
    </testcase>
  </testsuite>
</testsuites>
""")

        # Parse the XML
        import xml.etree.ElementTree as ET

        tree = ET.parse(junit_xml)
        root = tree.getroot()

        # Verify structure
        testsuites = root if root.tag == "testsuites" else root.find("testsuites")
        assert testsuites is not None

        testsuite = testsuites.find("testsuite") if testsuites is not None else root.find("testsuite")
        assert testsuite is not None

        testcases = testsuite.findall("testcase")
        assert len(testcases) == 2

    def test_parse_jest_junit_xml(self, tmp_path):
        """Test parsing Jest JUnit XML output."""
        skip_if_js_not_supported()

        # Create sample JUnit XML from jest-junit
        junit_xml = tmp_path / "junit.xml"
        junit_xml.write_text("""<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="2" failures="0" time="0.789">
  <testsuite name="math functions" tests="2" failures="0" time="0.456" timestamp="2024-01-01T00:00:00">
    <testcase classname="__tests__/math.test.js" name="add returns sum" time="0.123">
    </testcase>
    <testcase classname="__tests__/math.test.js" name="multiply returns product" time="0.234">
    </testcase>
  </testsuite>
</testsuites>
""")

        # Parse the XML
        import xml.etree.ElementTree as ET

        tree = ET.parse(junit_xml)
        root = tree.getroot()

        # Verify structure
        testsuites = root if root.tag == "testsuites" else root.find("testsuites")
        testsuite = testsuites.find("testsuite") if testsuites is not None else root.find("testsuite")
        assert testsuite is not None

        testcases = testsuite.findall("testcase")
        assert len(testcases) == 2
