"""Tests for JavaScript optimizer module.

Tests the code extraction and normalization functions without importing
the full optimizer module (to avoid LLM dependencies in tests).
"""

import re

# Pattern to extract code blocks from LLM response (copied from optimizer_javascript.py)
JS_CODE_PATTERN = re.compile(r"```(?:javascript|js|typescript|ts)(?::[^\n]*)?\s*\n(.*?)```", re.MULTILINE | re.DOTALL)


def extract_code_and_explanation(content: str) -> tuple[str, str]:
    """Extract code and explanation from LLM response."""
    match = JS_CODE_PATTERN.search(content)
    if match:
        code = match.group(1).strip()
        explanation_end = match.start()
        explanation = content[:explanation_end].strip()
        return code, explanation
    return "", content


def _normalize_code(code: str) -> str:
    """Normalize code for comparison."""
    # Remove single-line comments
    code = re.sub(r"//.*$", "", code, flags=re.MULTILINE)
    # Remove multi-line comments
    code = re.sub(r"/\*.*?\*/", "", code, flags=re.DOTALL)
    # Normalize whitespace
    code = " ".join(code.split())
    return code


class TestExtractCodeAndExplanation:
    """Tests for extracting code and explanation from LLM responses."""

    def test_extract_javascript_code_block(self) -> None:
        """Test extracting code from a JavaScript code block."""
        response = """**Optimization Explanation:**
I replaced the O(n²) nested loop with a more efficient Map-based lookup.

```javascript
function findDuplicates(arr) {
    const seen = new Map();
    const duplicates = [];
    for (const item of arr) {
        if (seen.has(item)) {
            duplicates.push(item);
        }
        seen.set(item, true);
    }
    return duplicates;
}
```
"""
        code, explanation = extract_code_and_explanation(response)

        assert "function findDuplicates" in code
        assert "new Map()" in code
        assert "O(n²)" in explanation or "Map-based" in explanation

    def test_extract_js_code_block(self) -> None:
        """Test extracting code from a ```js code block."""
        response = """Here's the optimized version:

```js
const add = (a, b) => a + b;
```
"""
        code, explanation = extract_code_and_explanation(response)

        assert "const add" in code
        assert "=>" in code

    def test_extract_typescript_code_block(self) -> None:
        """Test extracting code from a TypeScript code block."""
        response = """Optimized with types:

```typescript
function add(a: number, b: number): number {
    return a + b;
}
```
"""
        code, explanation = extract_code_and_explanation(response)

        assert "function add" in code
        assert ": number" in code

    def test_extract_ts_code_block(self) -> None:
        """Test extracting code from a ```ts code block."""
        response = """```ts
const multiply = (a: number, b: number): number => a * b;
```"""
        code, explanation = extract_code_and_explanation(response)

        assert "multiply" in code
        assert ": number" in code

    def test_extract_with_filename(self) -> None:
        """Test extracting code from a code block with filename."""
        response = """Here's the optimized code:

```javascript:utils.js
export function fibonacci(n) {
    if (n <= 1) return n;
    let a = 0, b = 1;
    for (let i = 2; i <= n; i++) {
        [a, b] = [b, a + b];
    }
    return b;
}
```
"""
        code, explanation = extract_code_and_explanation(response)

        assert "function fibonacci" in code
        assert "export" in code

    def test_no_code_block_returns_empty(self) -> None:
        """Test that missing code block returns empty code."""
        response = "This response has no code block, just explanation."

        code, explanation = extract_code_and_explanation(response)

        assert code == ""
        assert len(explanation) > 0

    def test_multiple_code_blocks_takes_first(self) -> None:
        """Test that only the first code block is extracted."""
        response = """First version:

```javascript
function first() { return 1; }
```

Alternative version:

```javascript
function second() { return 2; }
```
"""
        code, explanation = extract_code_and_explanation(response)

        assert "first" in code
        assert "second" not in code


class TestNormalizeCode:
    """Tests for code normalization for comparison."""

    def test_normalize_removes_single_line_comments(self) -> None:
        """Test that single-line comments are removed."""
        code = """
function add(a, b) {
    // This is a comment
    return a + b; // inline comment
}
"""
        normalized = _normalize_code(code)

        assert "//" not in normalized
        assert "This is a comment" not in normalized
        assert "inline comment" not in normalized

    def test_normalize_removes_multiline_comments(self) -> None:
        """Test that multi-line comments are removed."""
        code = """
/* This is a
   multi-line comment */
function add(a, b) {
    return a + b;
}
"""
        normalized = _normalize_code(code)

        assert "/*" not in normalized
        assert "*/" not in normalized
        assert "multi-line" not in normalized

    def test_normalize_whitespace(self) -> None:
        """Test that whitespace is normalized."""
        code1 = """
function   add(a,    b) {
    return    a + b;
}
"""
        code2 = "function add(a, b) { return a + b; }"

        assert _normalize_code(code1) == _normalize_code(code2)

    def test_identical_code_normalizes_same(self) -> None:
        """Test that identical code normalizes to the same string."""
        code1 = "function add(a, b) { return a + b; }"
        code2 = "function add(a, b) { return a + b; }"

        assert _normalize_code(code1) == _normalize_code(code2)

    def test_different_code_normalizes_different(self) -> None:
        """Test that different code normalizes to different strings."""
        code1 = "function add(a, b) { return a + b; }"
        code2 = "function subtract(a, b) { return a - b; }"

        assert _normalize_code(code1) != _normalize_code(code2)

    def test_normalize_preserves_string_contents(self) -> None:
        """Test that string contents are preserved."""
        code = 'const msg = "Hello // not a comment";'
        normalized = _normalize_code(code)

        # The string content should be preserved
        # (though this is a simplified test - real implementation
        # might handle this differently)
        assert "Hello" in normalized

    def test_normalize_empty_code(self) -> None:
        """Test that empty code normalizes to empty string."""
        assert _normalize_code("") == ""
        assert _normalize_code("   ") == ""
        assert _normalize_code("\n\n\n") == ""


class TestCodeDeduplication:
    """Tests for code deduplication logic."""

    def test_same_code_different_comments_equal(self) -> None:
        """Test that same code with different comments is considered equal."""
        code1 = """
// Version 1
function add(a, b) {
    return a + b;
}
"""
        code2 = """
// Version 2 - optimized
function add(a, b) {
    return a + b; // sum
}
"""
        assert _normalize_code(code1) == _normalize_code(code2)

    def test_same_code_different_whitespace_equal(self) -> None:
        """Test that same code with different whitespace is considered equal."""
        code1 = "function add(a, b) { return a + b; }"
        code2 = """
function add(a, b) {
    return a + b;
}
"""
        assert _normalize_code(code1) == _normalize_code(code2)

    def test_semantically_different_code_not_equal(self) -> None:
        """Test that semantically different code is not equal."""
        code1 = "function add(a, b) { return a + b; }"
        code2 = "function add(a, b) { return a - b; }"  # Different operation

        assert _normalize_code(code1) != _normalize_code(code2)
