from __future__ import annotations

import difflib
import io
from contextlib import redirect_stdout
from dataclasses import dataclass
from pathlib import Path

from .interpreter import Interpreter
from .lexer import Lexer
from .parser import ParseError, Parser


@dataclass(frozen=True)
class TestResult:
    path: Path
    passed: bool
    expected: str
    actual: str
    error: str | None = None


def _normalize(text: str) -> str:
    return text.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")


def _read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def run_test_file(path: Path) -> TestResult:
    expected_path = path.with_suffix(".out")
    expected = _normalize(_read_text(expected_path)) if expected_path.exists() else ""

    stdout_buf = io.StringIO()
    actual = ""
    error = None

    try:
        source = _read_text(path)
        tokens = Lexer(source).tokenize()
        program = Parser(tokens, source=source).parse()
        interpreter = Interpreter(base_dir=str(path.parent))
        with redirect_stdout(stdout_buf):
            interpreter.run(program)
        actual = _normalize(stdout_buf.getvalue())
    except Exception as e:
        # Treat any failure as test output so users can write negative tests.
        error = str(e)
        actual = _normalize(error)

    return TestResult(
        path=path,
        passed=(actual == expected),
        expected=expected,
        actual=actual,
        error=error,
    )


def discover_tests(tests_dir: Path) -> list[Path]:
    if not tests_dir.exists():
        return []
    files = sorted(tests_dir.rglob("*.prasad"))
    # Only run tests that have a corresponding golden output file.
    # This allows helper modules (import-only) to live in tests/ without failing.
    return [p for p in files if p.with_suffix(".out").exists()]


def run_tests(tests_dir: str = "tests") -> tuple[int, list[TestResult]]:
    base = Path(tests_dir)
    files = discover_tests(base)
    results = [run_test_file(p) for p in files]
    exit_code = 0 if all(r.passed for r in results) else 1
    return exit_code, results


def format_failure(result: TestResult) -> str:
    expected_lines = (result.expected + "\n").splitlines(keepends=True)
    actual_lines = (result.actual + "\n").splitlines(keepends=True)
    diff = difflib.unified_diff(
        expected_lines,
        actual_lines,
        fromfile="expected",
        tofile="actual",
    )
    return "".join(diff).rstrip("\n")
