"""
Nexa Language — Built-in Test Runner (v23)
`nexa test [file.nexa | directory] [--coverage]`

Features:
  - Discovers all test { "name" } blocks in .nexa files
  - Runs each with its own isolated environment
  - --coverage: counts executed lines vs total source lines
  - Reports: PASS/FAIL per test, summary, total coverage %
"""
from __future__ import annotations
import os, sys, time
from typing import List, Tuple, Optional


class CoverageTracker:
    """Track which source lines are executed during test runs."""

    def __init__(self, source: str):
        self.total_lines = len(source.splitlines())
        self.executed: set = set()

    def mark(self, line: int):
        self.executed.add(line)

    @property
    def pct(self) -> float:
        if self.total_lines == 0: return 100.0
        return round(100.0 * len(self.executed) / self.total_lines, 1)


class TestResult:
    def __init__(self, name: str, passed: bool, error: str = "", duration_ms: float = 0):
        self.name = name
        self.passed = passed
        self.error = error
        self.duration_ms = duration_ms

    def __repr__(self):
        icon = "✅" if self.passed else "❌"
        dur  = f"  ({self.duration_ms:.1f}ms)"
        err  = f"\n       ↳ {self.error}" if not self.passed else ""
        return f"  {icon} {'PASS' if self.passed else 'FAIL'}: {self.name}{dur}{err}"


class NexaTestRunner:
    """Discover and run Nexa test blocks with optional line coverage."""

    def __init__(self, coverage: bool = False, verbose: bool = False):
        self.coverage = coverage
        self.verbose = verbose
        self._results: List[TestResult] = []

    # ── Discovery ──────────────────────────────────────────────────────────────
    def discover(self, path: str) -> List[Tuple[str, str]]:
        """Return list of (file_path, source) for all .nexa files in path."""
        if os.path.isfile(path):
            if path.endswith(".nexa"):
                return [(path, open(path, encoding="utf-8").read())]
            return []
        files = []
        for root, _, names in os.walk(path):
            for name in names:
                if name.endswith(".nexa"):
                    fp = os.path.join(root, name)
                    files.append((fp, open(fp, encoding="utf-8").read()))
        return files

    # ── Main runner ───────────────────────────────────────────────────────────
    def run(self, target: str = ".") -> int:
        """Run all tests. Returns exit code (0=all pass, 1=failures)."""
        files = self.discover(target)
        if not files:
            print(f"  ⚠️  No .nexa files found in: {target}")
            return 0

        total_coverage = []
        print(f"\n  🧪 Nexa Test Runner v23\n  {'─' * 50}")

        for fp, src in files:
            if self.verbose:
                print(f"\n  📄 {fp}")
            cov = CoverageTracker(src) if self.coverage else None
            results = self._run_file(fp, src, cov)
            self._results.extend(results)
            if cov: total_coverage.append(cov)

        # ── Summary ───────────────────────────────────────────────────────────
        passed  = [r for r in self._results if r.passed]
        failed  = [r for r in self._results if not r.passed]
        total   = len(self._results)

        print(f"\n  {'─' * 50}")
        print(f"  Results: {len(passed)}/{total} passed", end="")
        if failed:
            print(f"  ({len(failed)} failed)")
        else:
            print("  🎉 All tests passed!")

        if self.coverage and total_coverage:
            combined_exec = set()
            combined_total = sum(c.total_lines for c in total_coverage)
            for c in total_coverage: combined_exec |= c.executed
            pct = round(100.0 * len(combined_exec) / max(1, combined_total), 1)
            bar_len = 30
            filled = int(bar_len * pct / 100)
            bar = "█" * filled + "░" * (bar_len - filled)
            color = "\033[92m" if pct >= 80 else "\033[93m" if pct >= 50 else "\033[91m"
            print(f"\n  📊 Coverage: {color}{bar}\033[0m {pct}%")

        return 0 if not failed else 1

    def _run_file(self, path: str, src: str, cov: Optional[CoverageTracker]) -> List[TestResult]:
        """Parse and run all test blocks in a single source file."""
        from nexa_lang.lexer import Lexer
        from nexa_lang.parser import Parser
        from nexa_lang.interpreter import Interpreter, Environment, NexaRuntimeError
        from nexa_lang.builtins import BUILTINS
        from nexa_lang.ast_nodes import TestBlock

        results = []
        import traceback
        try:
            tokens = Lexer(src).tokenize()
            ast = Parser(tokens).parse()
        except Exception as e:
            traceback.print_exc()
            results.append(TestResult(path, False, f"Parse error: {e}"))
            return results

        # Find TestBlock nodes
        test_nodes = [s for s in ast.statements if type(s).__name__ == "TestBlock"]
        if not test_nodes:
            return results

        # Build base env (execute non-test top-level statements once)
        base_interp = Interpreter()
        base_env = Environment()
        for k, v in BUILTINS.items(): base_env.set(k, v)

        non_test = [s for s in ast.statements if type(s).__name__ != "TestBlock"]
        for stmt in non_test:
            try: base_interp._exec(stmt, base_env)
            except Exception as e:
                traceback.print_exc()

        # Run each test block
        for node in test_nodes:
            start = time.perf_counter()
            interp = Interpreter()
            env = Environment(parent=base_env)
            if cov:
                # Patch _exec to track visited lines
                orig = interp._exec.__func__
                import types
                def cov_exec(self, node_, env_):
                    ln = getattr(node_, 'line', 0)
                    if ln: cov.mark(ln)
                    return orig(self, node_, env_)
                interp._exec = types.MethodType(cov_exec, interp)
            try:
                interp._exec_block(node.body, env)
                ms = (time.perf_counter() - start) * 1000
                r = TestResult(node.name, True, duration_ms=ms)
                print(repr(r))
            except Exception as e:
                ms = (time.perf_counter() - start) * 1000
                r = TestResult(node.name, False, str(e), ms)
                print(repr(r))
            results.append(r)

        return results


def run_tests(target: str = ".", coverage: bool = False, verbose: bool = False) -> int:
    return NexaTestRunner(coverage=coverage, verbose=verbose).run(target)
