"""AI-assisted test step generation.

For each discovered page, builds a compact structured page summary
(route, forms, buttons, inputs, API calls — NOT full source) and asks
Claude to generate Playwright-compatible test cases.

Token optimization: sends ~200-400 tokens per page, not full source.
"""

from __future__ import annotations

import json
import os
import re
from pathlib import Path

from maeris_mcp.tidy import SUPPORTED_EXTENSIONS, SKIP_DIRS

from .models import BaselineTest

BATCH_SIZE = int(os.getenv("MAERIS_TEST_GEN_BATCH_SIZE", "5"))

# ---------------------------------------------------------------------------
# Page summary extraction (deterministic)
# ---------------------------------------------------------------------------

_FORM_RE = re.compile(r'<form[^>]*(?:id|name|data-testid)=["\'`]([^"\'`]+)["\'`]', re.IGNORECASE)
_INPUT_RE = re.compile(
    r'<input[^>]*(?:name|placeholder|aria-label|data-testid)=["\'`]([^"\'`]+)["\'`]',
    re.IGNORECASE,
)
_BTN_LABEL_RE = re.compile(
    r'<button[^>]*>([^<]{2,40})</button>|'
    r'<button[^>]*(?:aria-label|data-testid)=["\'`]([^"\'`]+)["\'`]',
    re.IGNORECASE,
)
_FETCH_RE = re.compile(r'fetch\s*\(\s*["\'`]([^"\'`]+)["\'`]', re.IGNORECASE)
_AXIOS_RE = re.compile(
    r'axios\s*\.\s*(get|post|put|patch|delete)\s*\(\s*["\'`]([^"\'`]+)["\'`]',
    re.IGNORECASE,
)

_MAX_PAGE_CHARS = 60_000  # how much source to read per page summary


def _summarize_page(page_route: str, scan_root: Path) -> dict:
    """Deterministically extract a compact page summary from source files.

    Returns a structured dict:
    {
        "route": "/checkout",
        "forms": ["shipping_form", ...],
        "inputs": ["email", "password", ...],
        "buttons": ["Sign In", "Place Order", ...],
        "api_calls": ["POST /checkout", ...]
    }
    """
    forms: list[str] = []
    inputs: list[str] = []
    buttons: list[str] = []
    api_calls: list[str] = []
    total = 0

    # Normalise the route slug for file matching
    # e.g. /checkout → checkout, /auth/login → login, / → index
    route_slug = page_route.strip("/").split("/")[-1] or "index"

    for path in sorted(scan_root.rglob("*")):
        if not path.is_file() or path.suffix not in SUPPORTED_EXTENSIONS:
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        if total >= _MAX_PAGE_CHARS:
            break

        # Prioritise files whose name/path matches the route slug
        stem = path.stem.lower()
        rel = str(path.relative_to(scan_root)).lower()
        route_related = route_slug.lower() in stem or route_slug.lower() in rel

        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue

        total += len(content)

        if not route_related and total > 20_000:
            # Only scan non-related files early on
            continue

        for m in _FORM_RE.finditer(content):
            val = m.group(1).strip()
            if val and val not in forms:
                forms.append(val)

        for m in _INPUT_RE.finditer(content):
            val = m.group(1).strip()
            if val and val not in inputs:
                inputs.append(val)

        for m in _BTN_LABEL_RE.finditer(content):
            val = (m.group(1) or m.group(2) or "").strip()
            if val and val not in buttons:
                buttons.append(val)

        for m in _FETCH_RE.finditer(content):
            url = m.group(1).strip()
            if "/" in url and not url.startswith("http"):
                entry = f"fetch {url}"
                if entry not in api_calls:
                    api_calls.append(entry)

        for m in _AXIOS_RE.finditer(content):
            method = m.group(1).upper()
            url = m.group(2).strip()
            entry = f"{method} {url}"
            if entry not in api_calls:
                api_calls.append(entry)

    return {
        "route": page_route,
        "forms": forms[:10],
        "inputs": inputs[:15],
        "buttons": buttons[:15],
        "api_calls": api_calls[:10],
    }


# ---------------------------------------------------------------------------
# Claude prompt + generation
# ---------------------------------------------------------------------------

_GENERATION_PROMPT = """\
You are generating Playwright E2E test cases for page '{route}'.

Page structure summary:
{summary_json}

Generate test cases for this page. Rules:
- Include at least 1 positive (happy-path) test and 1 negative (validation/error) test.
- Each test must cover a distinct user journey on this page.
- Keep steps minimal and precise — prefer data-testid selectors when available.
- For negative tests, use invalid input or missing required fields.
- Steps must be executable by Playwright.

Return ONLY a JSON array of test cases (no prose, no markdown):
[
  {{
    "name": "Login: valid credentials",
    "description": "Happy-path login with correct email and password",
    "steps": [
      {{"action": "navigate", "selector": "", "value": "{base_url}{route}"}},
      {{"action": "fill", "selector": "[name=email]", "value": "test@example.com"}},
      {{"action": "fill", "selector": "[name=password]", "value": "Password123"}},
      {{"action": "click", "selector": "[type=submit]", "value": ""}}
    ],
    "assertions": [
      {{"type": "url", "selector": "", "value": "/dashboard"}},
      {{"type": "visible", "selector": "text=Welcome", "value": ""}}
    ]
  }},
  {{
    "name": "Login: empty fields",
    "description": "Negative test — submit with empty fields, expect validation error",
    "steps": [
      {{"action": "navigate", "selector": "", "value": "{base_url}{route}"}},
      {{"action": "click", "selector": "[type=submit]", "value": ""}}
    ],
    "assertions": [
      {{"type": "visible", "selector": "text=required", "value": ""}}
    ]
  }}
]
"""


def _call_claude(prompt: str, model: str) -> list[dict]:
    """Invoke Claude CLI and parse JSON list response."""
    from maeris_mcp.ai_adapter.repair_suggestions import _claude_run, _parse_json

    raw = _claude_run(prompt, model, timeout=180)
    result = _parse_json(raw)
    if not isinstance(result, list):
        raise ValueError(f"Expected JSON array, got {type(result).__name__}")
    return result


# ---------------------------------------------------------------------------
# Batch generation (reduces Claude CLI calls from N to N/BATCH_SIZE)
# ---------------------------------------------------------------------------

_BATCH_GENERATION_PROMPT = """\
You are generating Playwright E2E test cases for multiple pages in a single call.

For each page below, generate test cases following these rules:
- Include at least 1 positive (happy-path) test and 1 negative (validation/error) test per page.
- Each test must cover a distinct user journey on that page.
- Keep steps minimal and precise — prefer data-testid selectors when available.
- For negative tests, use invalid input or missing required fields.
- Steps must be executable by Playwright.

Pages to generate tests for:
{pages_json}

Return ONLY a JSON object keyed by route, where each value is an array of test cases:
{{
  "/login": [
    {{
      "name": "Login: valid credentials",
      "description": "Happy-path login",
      "steps": [
        {{"action": "navigate", "selector": "", "value": "{base_url}/login"}},
        {{"action": "fill", "selector": "[name=email]", "value": "test@example.com"}},
        {{"action": "click", "selector": "[type=submit]", "value": ""}}
      ],
      "assertions": [
        {{"type": "url", "selector": "", "value": "/dashboard"}}
      ]
    }}
  ],
  "/signup": [...]
}}
"""


def _call_claude_batch(prompt: str, model: str) -> dict[str, list[dict]]:
    """Invoke Claude CLI for multi-page generation and parse JSON dict response."""
    from maeris_mcp.ai_adapter.repair_suggestions import _claude_run, _parse_json

    raw = _claude_run(prompt, model, timeout=300)
    result = _parse_json(raw)
    if not isinstance(result, dict):
        raise ValueError(f"Expected JSON object keyed by route, got {type(result).__name__}")
    return result


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def generate_tests_for_discovery(
    pages: list[str],
    scan_root: Path,
    base_url: str,
    model: str,
    progress_callback=None,
) -> list[BaselineTest]:
    """For each page, extract a compact summary and ask Claude for test cases.

    When BATCH_SIZE > 1, groups pages into batches to reduce Claude CLI calls.
    Falls back to per-page generation on batch failure.

    Args:
        pages: List of page route paths to generate tests for.
        scan_root: Frontend source root for summary extraction.
        base_url: Application base URL (e.g. "http://localhost:3000").
        model: Claude model ID.
        progress_callback: Optional callable(page, test_count) for progress reporting.

    Returns:
        List of BaselineTest objects ready for execution.
    """
    base_url = base_url.rstrip("/")
    tests: list[BaselineTest] = []

    # Build summaries for all eligible pages
    page_summaries: list[tuple[str, dict]] = []
    for page in pages:
        summary = _summarize_page(page, scan_root)
        has_structure = any([summary["forms"], summary["inputs"], summary["buttons"], summary["api_calls"]])
        if has_structure or page == "/":
            page_summaries.append((page, summary))

    if BATCH_SIZE <= 1:
        # Original per-page path
        for page, summary in page_summaries:
            tests.extend(_generate_single_page(page, summary, base_url, model, progress_callback))
        return tests

    # Batch path: group pages into batches
    for batch_start in range(0, len(page_summaries), BATCH_SIZE):
        batch = page_summaries[batch_start:batch_start + BATCH_SIZE]
        batch_pages = {page: summary for page, summary in batch}

        pages_json = json.dumps(batch_pages, indent=2)
        prompt = _BATCH_GENERATION_PROMPT.format(
            pages_json=pages_json,
            base_url=base_url,
        )

        try:
            results_by_route = _call_claude_batch(prompt, model)
            for page, _ in batch:
                raw_cases = results_by_route.get(page, [])
                if not isinstance(raw_cases, list):
                    continue
                for case in raw_cases:
                    if not isinstance(case, dict) or not case.get("name"):
                        continue
                    bt = BaselineTest(
                        name=case.get("name", f"Test on {page}"),
                        page=page,
                        steps=case.get("steps") or [],
                        assertions=case.get("assertions") or [],
                    )
                    tests.append(bt)
                if progress_callback:
                    count = len([c for c in raw_cases if isinstance(c, dict)])
                    progress_callback(page, count)
        except Exception:
            # Fallback: generate per-page for this batch
            for page, summary in batch:
                tests.extend(_generate_single_page(page, summary, base_url, model, progress_callback))

    return tests


def _generate_single_page(
    page: str,
    summary: dict,
    base_url: str,
    model: str,
    progress_callback=None,
) -> list[BaselineTest]:
    """Generate tests for a single page (original per-page path)."""
    prompt = _GENERATION_PROMPT.format(
        route=page,
        summary_json=json.dumps(summary, indent=2),
        base_url=base_url,
    )

    try:
        raw_cases = _call_claude(prompt, model)
    except Exception:
        return []

    tests: list[BaselineTest] = []
    for case in raw_cases:
        if not isinstance(case, dict) or not case.get("name"):
            continue
        bt = BaselineTest(
            name=case.get("name", f"Test on {page}"),
            page=page,
            steps=case.get("steps") or [],
            assertions=case.get("assertions") or [],
        )
        tests.append(bt)

    if progress_callback:
        progress_callback(page, len([c for c in raw_cases if isinstance(c, dict)]))

    return tests
