#!/usr/bin/env python3
"""
Test tree-sitter-wasot parser against wasot parser using syntax test cases.

Parses test cases from tests/wasot_syntax_tests.md with both parsers and
compares the structural output.

Usage:
    python scripts/test_treesitter_parser.py                    # Run all tests
    python scripts/test_treesitter_parser.py -v                 # Verbose output
    python scripts/test_treesitter_parser.py --show-sexp        # Show s-expressions
    python scripts/test_treesitter_parser.py --test "Simple"    # Run specific test
"""

import argparse
import re
import subprocess
import sys
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

# Add scripts directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from parse_syntax_file import MarkdownTestParser, TestCase

# Global paths set during initialization
WASOT_BINARY: Path = None
TS_DIR: Path = None


@dataclass
class TestResult:
    """Result of a single test."""

    name: str
    passed: bool
    wasot_ok: bool
    ts_ok: bool
    differences: list[str] = field(default_factory=list)
    wasot_output: str = ""
    ts_output: str = ""


def run_wasot_sexp(input_text: str) -> tuple[bool, str]:
    """Run wasot parser with sexp output."""
    try:
        result = subprocess.run(
            [str(WASOT_BINARY), "-t", "sexp"],
            input=input_text,
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode != 0:
            return False, result.stderr.strip()
        return True, result.stdout.strip()
    except subprocess.TimeoutExpired:
        return False, "TIMEOUT"
    except Exception as e:
        return False, str(e)


def run_treesitter_parse(input_text: str) -> tuple[bool, str]:
    """Run tree-sitter parse on input text."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".wasot", delete=False) as f:
        f.write(input_text)
        temp_path = f.name

    try:
        result = subprocess.run(
            ["npx", "tree-sitter", "parse", temp_path],
            capture_output=True,
            text=True,
            timeout=10,
            cwd=TS_DIR,
        )
        output = result.stdout.strip()
        # Check for errors before filtering
        has_error = "ERROR" in output or "MISSING" in output
        # Remove any trailing stats line (e.g., "/tmp/file.wasot  Parse: 0.05 ms ...")
        lines = output.split('\n')
        filtered_lines = [l for l in lines if not l.startswith('/tmp/')]
        output = '\n'.join(filtered_lines)

        return not has_error, output
    except subprocess.TimeoutExpired:
        return False, "TIMEOUT"
    except Exception as e:
        return False, str(e)
    finally:
        Path(temp_path).unlink(missing_ok=True)


def pretty_print_sexp(sexp: str, indent: int = 2) -> str:
    """Pretty-print an s-expression with proper indentation."""
    result = []
    depth = 0
    i = 0
    in_string = False
    current_token = ""

    def flush_token():
        nonlocal current_token
        if current_token.strip():
            result.append(" " * (depth * indent) + current_token.strip())
        current_token = ""

    while i < len(sexp):
        char = sexp[i]

        if char == '"' and (i == 0 or sexp[i - 1] != '\\'):
            in_string = not in_string
            current_token += char
            i += 1
            continue

        if in_string:
            current_token += char
            i += 1
            continue

        if char == '(':
            flush_token()
            current_token = "("
            i += 1
            while i < len(sexp) and sexp[i] != '(' and sexp[i] != ')':
                if sexp[i] == '"':
                    in_string = True
                    current_token += sexp[i]
                    i += 1
                    while i < len(sexp) and in_string:
                        current_token += sexp[i]
                        if sexp[i] == '"' and sexp[i - 1] != '\\':
                            in_string = False
                        i += 1
                elif sexp[i] in '\n':
                    break
                else:
                    current_token += sexp[i]
                    i += 1
            depth += 1
        elif char == ')':
            if current_token.strip():
                result.append(" " * (depth * indent) + current_token.strip() + ")")
                current_token = ""
            else:
                if result:
                    result[-1] = result[-1] + ")"
            depth = max(0, depth - 1)
            i += 1
        elif char in ' \n\t':
            i += 1
        else:
            current_token += char
            i += 1

    flush_token()
    return "\n".join(result)


def extract_wasot_elements(sexp: str) -> list[dict]:
    """Extract structural elements from wasot sexp output."""
    elements = []

    quoted_str = r'"(?:[^"\\]|\\.)*"'

    # Block pattern: id is optional (some blocks have line_index instead)
    block_pattern = rf'\(block\s+type:\s+({quoted_str})\s+level:\s+(\d+)'
    for match in re.finditer(block_pattern, sexp):
        elements.append(
            {
                "kind": "block",
                "type": match.group(1).strip('"'),
                "level": int(match.group(2)),
            }
        )

    # Void pattern: id is optional
    void_pattern = rf'\(void\s+type:\s+({quoted_str})\s+level:\s+(\d+)'
    for match in re.finditer(void_pattern, sexp):
        elements.append(
            {
                "kind": "void",
                "type": match.group(1).strip('"'),
                "level": int(match.group(2)),
            }
        )

    context_pattern = r'\(context-line\s+type:\s+"([^"]+)"\s+level:\s+(\d+)'
    for match in re.finditer(context_pattern, sexp):
        elements.append(
            {"kind": "context", "type": match.group(1), "level": int(match.group(2))}
        )

    kwarg_pattern = r'\(kwarg\s+\(key\s+content:\s+"([^"]+)"[^)]*\)\s+\(value\s+content:\s+"([^"]+)"'
    for match in re.finditer(kwarg_pattern, sexp):
        elements.append({"kind": "kwarg", "key": match.group(1), "value": match.group(2)})

    inline_pattern = r'\(inline-text\s+content:\s+"([^"]*)"'
    for match in re.finditer(inline_pattern, sexp):
        elements.append({"kind": "inline-text", "content": match.group(1)})

    return elements


def extract_treesitter_elements(output: str) -> list[dict]:
    """Extract structural elements from tree-sitter parse output."""
    elements = []

    parts = re.split(r'(\(meta_line \[\d+, \d+\] - \[\d+, \d+\])', output)

    i = 1
    while i < len(parts):
        if '(meta_line' in parts[i]:
            meta_start = parts[i]
            content = parts[i + 1] if i + 1 < len(parts) else ""
            full_meta = meta_start + content

            element = parse_meta_line_content(full_meta)
            if element:
                elements.append(element)
        i += 2

    return elements


def parse_meta_line_content(content: str) -> Optional[dict]:
    """Parse content of a meta_line to extract element info."""
    dots_match = re.search(r'\(dots \[(\d+), (\d+)\] - \[(\d+), (\d+)\]\)', content)
    if not dots_match:
        return None

    start_col = int(dots_match.group(2))
    end_col = int(dots_match.group(4))
    level = end_col - start_col

    # Context line with content: ./type (not just ./ which is a close marker)
    if '(context_content' in content:
        return {"kind": "context", "level": level}

    # Close marker (./) - don't count as block or context
    if '(context_marker' in content:
        return None

    # Check for void marker
    is_void = '(void_marker' in content

    # Check for block_id and kwarg
    has_id = '(block_id' in content
    has_kwarg = '(kwarg' in content

    # Meta-line with just dots (no block_type) is still a block with empty type
    # Only skip if it's truly not a block (but all meta_lines are blocks in wasot)
    return {
        "kind": "void" if is_void else "block",
        "level": level,
        "has_id": has_id,
        "has_kwarg": has_kwarg,
    }


def compare_elements(
    wasot_elements: list[dict], ts_elements: list[dict]
) -> tuple[bool, list[str]]:
    """Compare elements from both parsers."""
    differences = []

    wasot_blocks = [e for e in wasot_elements if e["kind"] == "block"]
    ts_blocks = [e for e in ts_elements if e["kind"] == "block"]

    wasot_voids = [e for e in wasot_elements if e["kind"] == "void"]
    ts_voids = [e for e in ts_elements if e["kind"] == "void"]

    wasot_contexts = [e for e in wasot_elements if e["kind"] == "context"]
    ts_contexts = [e for e in ts_elements if e["kind"] == "context"]

    if len(wasot_blocks) != len(ts_blocks):
        differences.append(
            f"Block count mismatch: wasot={len(wasot_blocks)}, ts={len(ts_blocks)}"
        )

    if len(wasot_voids) != len(ts_voids):
        differences.append(
            f"Void count mismatch: wasot={len(wasot_voids)}, ts={len(ts_voids)}"
        )

    if len(wasot_contexts) != len(ts_contexts):
        differences.append(
            f"Context count mismatch: wasot={len(wasot_contexts)}, ts={len(ts_contexts)}"
        )

    wasot_levels = sorted([e["level"] for e in wasot_blocks])
    ts_levels = sorted([e["level"] for e in ts_blocks])

    if wasot_levels != ts_levels:
        differences.append(f"Block levels mismatch: wasot={wasot_levels}, ts={ts_levels}")

    return len(differences) == 0, differences


def run_test(tc: TestCase) -> TestResult:
    """Run a single test comparing both parsers."""
    result = TestResult(name=tc.name, passed=False, wasot_ok=False, ts_ok=False)

    wasot_ok, wasot_output = run_wasot_sexp(tc.input)
    result.wasot_ok = wasot_ok
    result.wasot_output = wasot_output

    ts_ok, ts_output = run_treesitter_parse(tc.input)
    result.ts_ok = ts_ok
    result.ts_output = ts_output

    if not wasot_ok:
        result.differences.append(f"Wasot parse failed: {wasot_output}")
        return result

    if not ts_ok:
        # Extract ERROR and MISSING nodes from the output (may have children, so no closing paren)
        error_nodes = re.findall(r'\((?:ERROR|MISSING) \[\d+, \d+\] - \[\d+, \d+\]', ts_output)
        if error_nodes:
            result.differences.append(f"Tree-sitter errors: {', '.join(e + ')' for e in error_nodes[:5])}")
        else:
            result.differences.append(f"Tree-sitter parse failed: {ts_output[:200]}")
        return result

    wasot_elements = extract_wasot_elements(wasot_output)
    ts_elements = extract_treesitter_elements(ts_output)

    passed, differences = compare_elements(wasot_elements, ts_elements)
    result.passed = passed
    result.differences = differences

    return result


def load_syntax_tests(md_path: Path) -> list[TestCase]:
    """Load test cases from wasot_syntax_tests.md."""
    content = md_path.read_text()
    parser = MarkdownTestParser(content)
    return parser.parse()


def build_wasot_binary(project_root: Path) -> Path:
    """Build wasot binary with sexp feature and return path."""
    print("Building wasot binary with sexp feature...")
    result = subprocess.run(
        ["cargo", "build", "--features", "sexp"],
        cwd=project_root,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        print(f"Failed to build: {result.stderr}", file=sys.stderr)
        sys.exit(1)
    return project_root / "target" / "debug" / "wasot"


def main():
    """Main entry point."""
    global WASOT_BINARY, TS_DIR

    parser = argparse.ArgumentParser(
        description="Test tree-sitter-wasot parser against wasot parser."
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Show verbose output"
    )
    parser.add_argument(
        "--show-sexp",
        action="store_true",
        help="Show pretty-printed s-expressions from both parsers",
    )
    parser.add_argument(
        "--test", type=str, default=None, help="Run only tests matching this string"
    )
    parser.add_argument(
        "--stop-on-fail", action="store_true", help="Stop on first failure"
    )
    parser.add_argument(
        "-j", "--jobs", type=int, default=8, help="Number of parallel jobs (default: 8)"
    )

    args = parser.parse_args()

    project_root = Path(__file__).parent.parent
    TS_DIR = project_root / "tree-sitter-wasot"
    tests_file = project_root / "tests" / "wasot_syntax_tests.md"

    if not TS_DIR.exists():
        print(f"Error: {TS_DIR} does not exist", file=sys.stderr)
        sys.exit(1)

    if not tests_file.exists():
        print(f"Error: {tests_file} does not exist", file=sys.stderr)
        sys.exit(1)

    # Build wasot binary
    WASOT_BINARY = build_wasot_binary(project_root)
    if not WASOT_BINARY.exists():
        print(f"Error: Binary not found at {WASOT_BINARY}", file=sys.stderr)
        sys.exit(1)

    test_cases = load_syntax_tests(tests_file)

    if not test_cases:
        print("No tests found", file=sys.stderr)
        sys.exit(1)

    # Filter tests if specified
    if args.test:
        test_cases = [tc for tc in test_cases if args.test.lower() in tc.name.lower()]

    print(f"Running {len(test_cases)} tests from wasot_syntax_tests.md (parallel={args.jobs})...\n")

    # Run tests in parallel
    results: list[tuple[TestCase, TestResult]] = []

    with ThreadPoolExecutor(max_workers=args.jobs) as executor:
        future_to_tc = {executor.submit(run_test, tc): tc for tc in test_cases}

        for future in as_completed(future_to_tc):
            tc = future_to_tc[future]
            try:
                result = future.result()
                results.append((tc, result))
            except Exception as e:
                result = TestResult(name=tc.name, passed=False, wasot_ok=False, ts_ok=False)
                result.differences.append(f"Exception: {e}")
                results.append((tc, result))

    # Sort results by original order
    tc_order = {tc.name: i for i, tc in enumerate(test_cases)}
    results.sort(key=lambda x: tc_order.get(x[0].name, 0))

    # Print results
    passed_count = 0
    failed_count = 0

    for tc, result in results:
        if result.passed:
            passed_count += 1
            if args.verbose:
                print(f"  {tc.name}")
        else:
            failed_count += 1
            print(f"\n{'=' * 60}")
            print(f"FAILED: {tc.name}")
            print(f"{'=' * 60}")
            print(f"\nInput:")
            for line in tc.input.split('\n'):
                print(f"  {repr(line)}")
            if result.differences:
                print(f"\nDifferences:")
                for diff in result.differences:
                    print(f"  - {diff}")
            print(f"\nWasot S-expression (expected):")
            print(pretty_print_sexp(result.wasot_output))
            print(f"\nTree-sitter S-expression (actual):")
            print(result.ts_output)
            print()

        if args.show_sexp:
            print(f"\n{'=' * 60}")
            print(f"TEST: {tc.name}")
            if tc.chapter:
                print(f"Chapter: {tc.chapter}")
            print(f"{'=' * 60}")
            print(f"\nInput:")
            print("-" * 40)
            for line in tc.input.split('\n'):
                print(f"  {repr(line)}")
            print("-" * 40)
            print(f"\nWasot S-expression:")
            print("-" * 40)
            print(pretty_print_sexp(result.wasot_output))
            print("-" * 40)
            print(f"\nTree-sitter S-expression:")
            print("-" * 40)
            print(result.ts_output)
            print("-" * 40)

    print(f"\n{'=' * 60}")
    print("SUMMARY")
    print(f"{'=' * 60}")
    print(f"Total:  {len(results)}")
    print(f"Passed: {passed_count}")
    print(f"Failed: {failed_count}")

    if failed_count > 0:
        print(f"\nFailed tests:")
        for tc, r in results:
            if not r.passed:
                print(f"  - {r.name}")
        sys.exit(1)
    else:
        print(f"\n All tests passed!")
        sys.exit(0)


if __name__ == "__main__":
    main()
