"""Pipeline orchestrator for baseline test generation.

Executes the full pipeline:
  1. discover_journeys() → deterministic page/route graph
  2. generate_tests_for_discovery() → AI generates test steps (one call per page)
  3. generate_scripts() → write Playwright .py files
  4. validate_stability() → 3 consecutive passes required
  5. attempt_repair() for each failing test (max 1 attempt each)
  6. validate_stability() again for repaired tests
  7. Push stable tests to Maeris via client.create_manual_tests()

Hard constraints enforced here:
  - Never push a test with pass_count < REQUIRED_PASSES
  - Never call repair on a test that already has repair_attempts >= 1
  - --dry-run skips step 7 only
"""

from __future__ import annotations

import tempfile
from pathlib import Path

from .models import BaselineResult, BaselineTest, TestStatus
from .journey_discovery import discover_journeys
from .test_generator import generate_tests_for_discovery
from .test_executor import generate_scripts
from .stability_validator import validate_stability, REQUIRED_PASSES
from .repair_engine import attempt_repair


# ---------------------------------------------------------------------------
# Maeris payload builder
# ---------------------------------------------------------------------------

_ACTION_MAP = {
    "navigate": "GoTo",
    "click": "Click",
    "fill": "Type",
    "type": "Type",
    "select": "Select",
    "hover": "Hover",
    "press": "KeyPress",
    "check": "Check",
    "uncheck": "Uncheck",
    "clear": "Clear",
}

_ASSERTION_TYPE_MAP = {
    "url": "to have url",
    "text": "to have text",
    "visible": "to be visible",
    "hidden": "to be hidden",
    "element": "to be visible",
    "value": "to have value",
}


def _build_maeris_steps(test: BaselineTest, base_url: str) -> list[dict]:
    """Convert BaselineTest steps + assertions into Maeris manual step payloads."""
    base_url = base_url.rstrip("/")
    maeris_steps: list[dict] = []
    current_url = base_url + test.page

    for step in test.steps:
        action = step.get("action", "").lower()
        selector = step.get("selector", "")
        value = step.get("value", "")
        description = step.get("description", "") or action

        action_type = _ACTION_MAP.get(action, "Click")

        payload: dict = {
            "name": description or f"{action_type} {selector}".strip(),
            "actionType": action_type,
        }
        if selector:
            payload["cssSelector"] = selector
        if value:
            if action in ("fill", "type", "press"):
                payload["mock_input"] = value
            elif action == "navigate":
                payload["page_url"] = value
                current_url = value
        if current_url:
            payload["page_url"] = current_url
        payload["page_name"] = test.page

        maeris_steps.append(payload)

    for assertion in test.assertions:
        atype = assertion.get("type", "visible").lower()
        selector = assertion.get("selector", "")
        value = assertion.get("value", "")

        assertion_type = _ASSERTION_TYPE_MAP.get(atype, "to be visible")
        payload = {
            "type": "verification",
            "name": f"Assert {atype}" + (f": {value}" if value else ""),
            "assertion_nature": "element" if selector else "url",
            "assertion_type": assertion_type,
        }
        if selector:
            payload["css_selector"] = selector
        if value:
            payload["assertion_value"] = value
        if current_url:
            payload["page_url"] = current_url
        payload["page_name"] = test.page

        maeris_steps.append(payload)

    return maeris_steps


# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------


async def generate_baseline(
    client,
    scan_root: Path,
    base_url: str,
    model: str,
    dry_run: bool,
    browser: str = "chromium",
    folder_name: str = "Baseline",
    progress_callback=None,
) -> BaselineResult:
    """Run the full baseline generation pipeline.

    Args:
        client: Authenticated MaerisClient instance.
        scan_root: Path to the frontend source root.
        base_url: Application base URL (e.g. "https://light-dev.up.railway.app").
        model: Claude model ID for generation and repair.
        dry_run: If True, skip pushing to Maeris.
        browser: Playwright browser engine.
        folder_name: Maeris folder/group name for pushed tests.
        progress_callback: Optional callable(stage: str, detail: str).

    Returns:
        BaselineResult with counts and test status list.
    """
    def _emit(stage: str, detail: str = "") -> None:
        if progress_callback:
            progress_callback(stage, detail)

    # ------------------------------------------------------------------
    # Step 1: Discover journeys (deterministic)
    # ------------------------------------------------------------------
    _emit("discover", f"Scanning {scan_root}...")
    discovery = discover_journeys(scan_root)
    _emit(
        "discover_done",
        f"Found {len(discovery.pages)} pages, "
        f"{len(discovery.nav_links)} nav links, "
        f"{discovery.files_scanned} files scanned.",
    )

    if not discovery.pages:
        return BaselineResult(total=0)

    # ------------------------------------------------------------------
    # Step 2: Generate test cases (AI per page)
    # ------------------------------------------------------------------
    _emit("generate", f"Generating tests for {len(discovery.pages)} pages ({model})...")

    def _gen_progress(page: str, count: int) -> None:
        _emit("generate_page", f"  {page} → {count} test(s)")

    tests = generate_tests_for_discovery(
        pages=discovery.pages,
        scan_root=scan_root,
        base_url=base_url,
        model=model,
        progress_callback=_gen_progress,
    )
    _emit("generate_done", f"Generated {len(tests)} tests.")

    if not tests:
        return BaselineResult(total=0)

    result = BaselineResult(total=len(tests), tests=tests)

    # ------------------------------------------------------------------
    # Step 3: Generate Playwright scripts
    # ------------------------------------------------------------------
    with tempfile.TemporaryDirectory(prefix="maeris_baseline_") as tmpdir:
        output_dir = Path(tmpdir)
        _emit("scripts", "Writing Playwright scripts...")
        generate_scripts(tests, base_url, output_dir)

        # ------------------------------------------------------------------
        # Step 4: First stability validation (3 consecutive passes)
        # ------------------------------------------------------------------
        _emit("validate", "Running stability validation (3 passes required)...")

        def _stability_cb(name: str, run_num: int, passed: bool) -> None:
            icon = "✓" if passed else "✗"
            _emit("run", f"  {icon}  {name:<40}  pass {run_num}/{REQUIRED_PASSES}")

        validate_stability(tests, browser=browser, progress_callback=_stability_cb)

        # ------------------------------------------------------------------
        # Step 5: Repair failing tests (max 1 attempt each)
        # ------------------------------------------------------------------
        failing = [
            t for t in tests
            if t.status in (TestStatus.FAILING, TestStatus.PENDING)
            and t.repair_attempts == 0
            and t.status != TestStatus.UNSTABLE
        ]

        if failing:
            _emit("repair", f"Attempting repair for {len(failing)} failing test(s)...")
            for t in failing:
                repaired = attempt_repair(
                    t,
                    scan_root=scan_root,
                    base_url=base_url,
                    output_dir=output_dir,
                    model=model,
                )
                if repaired:
                    _emit("repair_ok", f"  ✓  {t.name} — repaired, re-validating...")
                    # Re-validate this single test
                    validate_stability(
                        [t],
                        browser=browser,
                        progress_callback=_stability_cb,
                    )
                else:
                    _emit("repair_fail", f"  ✗  {t.name} — discarded")

        # ------------------------------------------------------------------
        # Step 6: Collect stable tests
        # ------------------------------------------------------------------
        stable = [t for t in tests if t.pass_count >= REQUIRED_PASSES]
        unstable = [t for t in tests if t.pass_count < REQUIRED_PASSES]

        result.pushed = len(stable)
        result.unstable = len(unstable)

        for t in stable:
            t.status = TestStatus.PASSING
        for t in unstable:
            t.status = TestStatus.UNSTABLE

        _emit(
            "summary",
            f"{len(stable)} stable, {len(unstable)} discarded.",
        )

        # ------------------------------------------------------------------
        # Step 7: Push to Maeris (skip if dry_run)
        # ------------------------------------------------------------------
        if dry_run:
            _emit("dry_run", "--dry-run: skipping Maeris push.")
            return result

        if not stable:
            _emit("push_skip", "No stable tests to push.")
            return result

        _emit("push", f"Pushing {len(stable)} tests to Maeris (folder: {folder_name})...")

        # Resolve or create folder
        try:
            groups = await client.get_testcase_groups()
            matched = next(
                (g for g in groups if (g.get("name") or "").lower() == folder_name.lower()),
                None,
            )
            if matched:
                folder_id = (
                    matched.get("testcase_group_id") or matched.get("id") or ""
                )
            else:
                folder_id = await client.create_testcase_group(folder_name)
        except Exception:
            folder_id = ""

        payloads = []
        for t in stable:
            maeris_steps = _build_maeris_steps(t, base_url)
            if not maeris_steps:
                result.skipped += 1
                continue
            payloads.append({
                "name": t.name,
                "description": t.name,
                "testcase_type": "regression",
                "test_folder": folder_id,
                "labels": ["baseline"],
                "browser": ["chrome"],
                "mark_as": "pass",
                "steps": maeris_steps,
            })

        if payloads:
            try:
                push_result = await client.create_manual_tests(payloads)
                pushed_ids = []
                if isinstance(push_result, list):
                    pushed_ids = [
                        (r.get("testcase_id") or r.get("_id") or "")
                        for r in push_result
                    ]
                elif isinstance(push_result, dict):
                    pushed_ids = [push_result.get("testcase_id") or ""]

                for t, tid in zip(stable, pushed_ids):
                    t.maeris_testcase_id = tid
                    t.status = TestStatus.PUSHED

                _emit("push_done", f"✓ {len(payloads)} tests pushed to Maeris.")
            except Exception as exc:
                _emit("push_error", f"✗ Push failed: {exc}")

    return result
