#!/usr/bin/env python3
"""
Roundtrip tests for wasot syntax test cases.

Reads test cases from wasot_syntax_tests.md and verifies roundtrip conversions:
- wasot -> pretty -> binary -> wasot -> pretty
- wasot -> pretty -> jsonl -> wasot -> pretty
- wasot -> pretty -> xml -> wasot -> pretty

Verifies that the two pretty-printed outputs are identical for each format.
"""
# ruff: noqa: T201

import argparse
import os
import re
import shlex
import subprocess
import sys
import tempfile
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Any

# Global variable for wasot command (set by argparse)
WASOT_CMD: list[str] = []


@dataclass
class TestCase:
    """Represents a single test case."""

    name: str
    description: str
    input: str
    expected_events: list[dict[str, Any]]
    chapter: str = ""
    section_number: int = 0


class MarkdownTestParser:
    """Parser for markdown test files."""

    def __init__(self, content: str):
        self.content = content
        self.lines = content.split("\n")
        self.current_line = 0
        self.current_chapter = ""
        self.section_counter = 0

    def parse(self) -> list[TestCase]:
        """Parse all test cases from the markdown content."""
        test_cases = []

        while self.current_line < len(self.lines):
            line = self.lines[self.current_line]

            # Check for chapter heading (## Chapter N: ...) or any ## heading
            if line.startswith("## "):
                self.current_chapter = self._extract_chapter_name(line)
                self.section_counter = 0
                self.current_line += 1
                continue

            # Check for test case (starts with **)
            if line.startswith("**") and line.endswith("**") and len(line) > 4:
                test_case = self._parse_test_case()
                if test_case:
                    test_case.chapter = self.current_chapter
                    test_case.section_number = self.section_counter
                    test_cases.append(test_case)
                    self.section_counter += 1
                # Note: _parse_test_case advances current_line
            else:
                self.current_line += 1

        return test_cases

    def _extract_chapter_name(self, line: str) -> str:
        """Extract chapter name from heading."""
        match = re.match(r"## Chapter \d+: (.+)", line)
        if match:
            return match.group(1)
        return line.replace("## ", "")

    def _parse_test_case(self) -> TestCase | None:
        """Parse a single test case starting at current line."""
        # Extract test name
        line = self.lines[self.current_line]
        name = line[2:-2]  # Remove ** from both ends
        self.current_line += 1

        # Skip empty line if present
        if (
            self.current_line < len(self.lines)
            and not self.lines[self.current_line].strip()
        ):
            self.current_line += 1

        # Collect description until we hit code block
        description_lines = []
        while self.current_line < len(self.lines):
            line = self.lines[self.current_line]

            # Stop at next test case or code block or heading
            if (
                line.startswith("```wasot")
                or (line.startswith("**") and line.endswith("**"))
                or line.startswith("#")
            ):
                break

            # Skip separator lines
            if line.strip() != "---":
                description_lines.append(line)
            self.current_line += 1

        description = "\n".join(description_lines).strip()

        # Parse wasot input block
        if not self._expect_line("```wasot"):
            return None

        input_lines = []
        while self.current_line < len(self.lines):
            line = self.lines[self.current_line]
            self.current_line += 1
            if line.startswith("```"):
                break
            input_lines.append(line)

        input_text = "\n".join(input_lines)

        # Skip the rest (yaml block, etc.) - we just need the input
        while self.current_line < len(self.lines):
            line = self.lines[self.current_line]
            if line.strip() == "---":
                self.current_line += 1
                break
            if line.startswith("**") and line.endswith("**"):
                break
            self.current_line += 1

        return TestCase(
            name=name,
            description=description,
            input=input_text,
            expected_events=[],  # Not needed for roundtrip tests
            chapter=self.current_chapter,
            section_number=self.section_counter,
        )

    def _expect_line(self, expected: str) -> bool:
        """Check if current line matches expected string."""
        if self.current_line >= len(self.lines):
            return False

        line = self.lines[self.current_line]
        if line.startswith(expected):
            self.current_line += 1
            return True
        return False


def run_wasot(
    wasot_cmd: list[str], *args: str, input_text: str | None = None
) -> subprocess.CompletedProcess:
    """Run wasot command and return result."""
    cmd = [*wasot_cmd, *args]
    return subprocess.run(
        cmd, capture_output=True, text=True, input=input_text, timeout=30
    )


def strip_document_id(content: str) -> str:
    """Strip the auto-generated document ID line (./id ...) from the content.

    The document ID is auto-generated and will differ between runs,
    so we strip it before comparing roundtrip outputs.
    """
    lines = content.splitlines()
    # Filter out lines that are document ID declarations (./id ...)
    # These are at the document level (not block IDs like .block :id)
    filtered = [line for line in lines if not line.startswith("./id ")]
    return "\n".join(filtered)


def roundtrip_binary(
    wasot_cmd: list[str], _input_text: str, tmpdir: Path, pretty1: str, _test_name: str
) -> tuple[bool, str]:
    """Test binary roundtrip: pretty -> binary -> pretty."""
    binary_file = tmpdir / "converted.wasotb"
    pretty2_file = tmpdir / "pretty2_binary.wasot"

    # Convert to binary format
    result = run_wasot(wasot_cmd, "-t", "binary", "-o", str(binary_file), input_text=pretty1)
    if result.returncode != 0:
        return False, f"Binary conversion failed: {result.stderr}"

    # Convert binary back to wasot and pretty print
    result = run_wasot(wasot_cmd, "-f", "binary", str(binary_file), "-o", str(pretty2_file))
    if result.returncode != 0:
        return False, f"Binary to wasot failed: {result.stderr}"

    # Compare the two pretty-printed outputs
    pretty2 = pretty2_file.read_text()
    content1 = strip_document_id(pretty1)
    content2 = strip_document_id(pretty2)
    if content1 == content2:
        return True, ""
    return False, f"Outputs differ ({len(content1)} vs {len(content2)} bytes)"


def roundtrip_jsonl(
    wasot_cmd: list[str], _input_text: str, tmpdir: Path, pretty1: str, _test_name: str
) -> tuple[bool, str]:
    """Test JSONL roundtrip: pretty -> jsonl -> pretty."""
    jsonl_file = tmpdir / "converted.jsonl"
    pretty2_file = tmpdir / "pretty2_jsonl.wasot"

    # Convert to JSONL format
    result = run_wasot(wasot_cmd, "-t", "jsonl", "-o", str(jsonl_file), input_text=pretty1)
    if result.returncode != 0:
        return False, f"JSONL conversion failed: {result.stderr}"

    # Convert JSONL back to wasot and pretty print
    result = run_wasot(wasot_cmd, "-f", "jsonl", str(jsonl_file), "-o", str(pretty2_file))
    output_combined = result.stdout + result.stderr
    if result.returncode != 0 or "error" in output_combined.lower():
        return False, f"JSONL to wasot failed: {output_combined.strip()}"

    # Compare the two pretty-printed outputs
    pretty2 = pretty2_file.read_text()
    content1 = strip_document_id(pretty1)
    content2 = strip_document_id(pretty2)
    if content1 == content2:
        return True, ""
    return False, f"Outputs differ ({len(content1)} vs {len(content2)} bytes)"


def roundtrip_xml(
    wasot_cmd: list[str], _input_text: str, tmpdir: Path, pretty1: str, _test_name: str
) -> tuple[bool, str]:
    """Test XML roundtrip: pretty -> xml -> pretty."""
    xml_file = tmpdir / "converted.xml"
    pretty2_file = tmpdir / "pretty2_xml.wasot"

    # Convert to XML format
    result = run_wasot(wasot_cmd, "-t", "xml", "-o", str(xml_file), input_text=pretty1)
    if result.returncode != 0:
        return False, f"XML conversion failed: {result.stderr}"

    # Convert XML back to wasot and pretty print
    result = run_wasot(wasot_cmd, "-f", "xml", str(xml_file), "-o", str(pretty2_file))
    if result.returncode != 0:
        return False, f"XML to wasot failed: {result.stderr}"

    # Compare the two pretty-printed outputs
    pretty2 = pretty2_file.read_text()
    content1 = strip_document_id(pretty1)
    content2 = strip_document_id(pretty2)
    if content1 == content2:
        return True, ""
    return False, f"Outputs differ ({len(content1)} vs {len(content2)} bytes)"


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

    test_name: str
    display_name: str
    results: dict[str, tuple[bool, str]]  # format -> (passed, error_msg)


def roundtrip_test(
    wasot_cmd: list[str], test_case: TestCase, display_name: str
) -> TestResult:
    """
    Test roundtrip conversion for a single test case using all formats.
    Returns a TestResult with format name to (success, error) mapping.
    """
    results: dict[str, tuple[bool, str]] = {}

    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)

        # Step 1: Pretty print original wasot input
        result = run_wasot(wasot_cmd, input_text=test_case.input)
        if result.returncode != 0:
            error_msg = f"Pretty print failed: {result.stderr}"
            return TestResult(
                test_name=test_case.name,
                display_name=display_name,
                results={
                    "binary": (False, error_msg),
                    "jsonl": (False, error_msg),
                    "xml": (False, error_msg),
                },
            )

        pretty1 = result.stdout

        # Test each format
        results["binary"] = roundtrip_binary(
            wasot_cmd, test_case.input, tmpdir_path, pretty1, test_case.name
        )
        results["jsonl"] = roundtrip_jsonl(
            wasot_cmd, test_case.input, tmpdir_path, pretty1, test_case.name
        )
        results["xml"] = roundtrip_xml(
            wasot_cmd, test_case.input, tmpdir_path, pretty1, test_case.name
        )

    return TestResult(
        test_name=test_case.name,
        display_name=display_name,
        results=results,
    )


def run_single_test(args: tuple[list[str], TestCase, str]) -> TestResult:
    """Wrapper function for parallel execution."""
    wasot_cmd, test_case, display_name = args
    return roundtrip_test(wasot_cmd, test_case, display_name)


def main():
    global WASOT_CMD

    arg_parser = argparse.ArgumentParser(
        description="Roundtrip tests for wasot syntax test cases."
    )
    arg_parser.add_argument(
        "-e", "--executable",
        help="Path to wasot executable or command (default: 'cargo run --bin wasot --')",
        default=None,
    )
    arg_parser.add_argument(
        "-j", "--jobs",
        type=int,
        default=os.cpu_count(),
        help=f"Number of parallel jobs (default: {os.cpu_count()})",
    )
    args = arg_parser.parse_args()

    if not args.executable:
        os.system("cargo build --bin wasot")
        args.executable = "./target/debug/wasot"

    # Parse the executable argument - could be a path or a command with args
    wasot_cmd = shlex.split(args.executable)
    WASOT_CMD = wasot_cmd  # Keep for backwards compatibility

    # Find the markdown test file
    script_dir = Path(__file__).parent
    md_file = script_dir / "wasot_syntax_tests.md"

    if not md_file.exists():
        print(f"Error: {md_file} not found")
        sys.exit(1)

    # Parse test cases
    content = md_file.read_text()
    md_parser = MarkdownTestParser(content)
    test_cases = md_parser.parse()

    if not test_cases:
        print("No test cases found in markdown file")
        sys.exit(1)

    formats = ["binary", "jsonl", "xml"]
    num_workers = args.jobs or os.cpu_count() or 1
    print(
        f"Testing roundtrip for {len(test_cases)} syntax test cases "
        f"({', '.join(formats)}) using {num_workers} workers...\n"
    )

    # Prepare test arguments
    test_args: list[tuple[list[str], TestCase, str]] = []
    for test_case in test_cases:
        if test_case.chapter:
            display_name = f"{test_case.chapter}/{test_case.name}"
        else:
            display_name = test_case.name
        test_args.append((wasot_cmd, test_case, display_name))

    # Track results per format
    passed = {fmt: 0 for fmt in formats}
    failed = {fmt: 0 for fmt in formats}
    failed_tests: list[tuple[str, str, str]] = []  # (name, format, error)

    # Run tests in parallel
    completed = 0
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        futures = {executor.submit(run_single_test, arg): arg for arg in test_args}

        for future in as_completed(futures):
            completed += 1
            result = future.result()

            # Collect status for each format
            status_parts = []
            all_ok = True
            for fmt in formats:
                success, error_msg = result.results.get(fmt, (False, "Unknown error"))
                if success:
                    passed[fmt] += 1
                    status_parts.append(f"{fmt}:OK")
                else:
                    failed[fmt] += 1
                    status_parts.append(f"{fmt}:FAIL")
                    failed_tests.append((result.display_name, fmt, error_msg))
                    all_ok = False

            # Print progress
            status = "OK" if all_ok else f"[{', '.join(status_parts)}]"
            print(f"[{completed}/{len(test_cases)}] {result.display_name}... {status}")

    # Print summary
    print("\n" + "=" * 60)
    print("Results:")
    total_failed = 0
    for fmt in formats:
        total = passed[fmt] + failed[fmt]
        status = "PASS" if failed[fmt] == 0 else "FAIL"
        print(f"  {fmt}: {passed[fmt]}/{total} passed [{status}]")
        total_failed += failed[fmt]

    if failed_tests:
        print(f"\nFailed tests ({len(failed_tests)}):")
        for test_name, fmt, error_msg in failed_tests:
            print(f"  - {test_name} ({fmt}): {error_msg}")

    if total_failed > 0:
        print(f"\n{total_failed} failures total")
        sys.exit(1)
    else:
        print(f"\nAll {len(test_cases)} tests passed for all formats!")


if __name__ == "__main__":
    main()
