"""AI-powered test case extraction using the Claude Code CLI (`claude -p`)."""

from __future__ import annotations

import json
import re
import shutil
import subprocess
from pathlib import Path

from maeris_mcp.importers.test_importer import (
    ImportedAssertion,
    ImportedStep,
    ImportedTestCase,
)
from maeris_mcp.types.test_generation import ActionType


_ACTION_MAP: dict[str, ActionType] = {
    "go_to": ActionType.GO_TO,
    "click": ActionType.CLICK,
    "send_keys": ActionType.SEND_KEYS,
    "hover": ActionType.HOVER,
    "upload": ActionType.UPLOAD,
    "screenshot": ActionType.SCREENSHOT,
}


def _build_instruction(file_name: str) -> str:
    """Short instruction passed as the -p argument. File content is piped via stdin."""
    return (
        f"You are a test case extractor. The content of a test file named `{file_name}` "
        "will be provided to you via stdin. Analyse it and extract every test case it contains.\n\n"
        "Return ONLY a valid JSON array — no prose, no explanation, no markdown fences.\n\n"
        "Each element of the array must follow this exact schema:\n"
        "{\n"
        '  "name": "Short descriptive test case name",\n'
        '  "steps": [\n'
        "    {\n"
        '      "action": "<go_to|click|send_keys|hover|upload|screenshot>",\n'
        '      "description": "Human-readable step description",\n'
        '      "selector": "<CSS/XPath selector or null>",\n'
        '      "value": "<input value or null>",\n'
        '      "url": "<full URL for go_to steps or null>"\n'
        "    }\n"
        "  ],\n"
        '  "assertions": [\n'
        "    {\n"
        '      "nature": "<existence|url_check|text_content|input_value>",\n'
        '      "atype": "<to be visible|not to be visible|equals|contains>",\n'
        '      "value": "<expected value or null>",\n'
        '      "selector": "<selector or null>",\n'
        '      "description": "What is being verified"\n'
        "    }\n"
        "  ]\n"
        "}\n\n"
        "Rules:\n"
        "- Extract ONLY what is present in the stdin content — do not invent steps.\n"
        "- Every meaningful action in the file must become a step.\n"
        "- Each test function / test block / script section = one test case.\n"
        "- If the file is a single script with no function wrappers, return one test case.\n"
        "- Use the test function name or a descriptive name derived from the script as the test name.\n"
        "- For Selenium: driver.get() → go_to, find_element + send_keys → send_keys, .click() → click.\n"
        "- For Playwright: page.goto() → go_to, page.fill() → send_keys, page.click() → click.\n"
        "- For assertions: is_visible/is_displayed → existence/to be visible, "
        "url checks → url_check, text checks → text_content."
    )


def _map_step(s: dict, base_url: str) -> ImportedStep:
    action = _ACTION_MAP.get(s.get("action", ""), ActionType.CLICK)
    url = s.get("url") or None
    if url and base_url and url.startswith("/"):
        url = base_url.rstrip("/") + url
    return ImportedStep(
        action=action,
        description=s.get("description", ""),
        selector=s.get("selector") or None,
        value=s.get("value") or None,
        url=url,
    )


def _map_assertion(a: dict) -> ImportedAssertion:
    return ImportedAssertion(
        nature=a.get("nature", "existence"),
        atype=a.get("atype", "to be visible"),
        value=a.get("value") or None,
        selector=a.get("selector") or None,
        description=a.get("description") or None,
    )


def _to_imported(tc: dict, source_path: str, base_url: str) -> ImportedTestCase:
    steps = [_map_step(s, base_url) for s in tc.get("steps", [])]
    assertions = [_map_assertion(a) for a in tc.get("assertions", [])]
    return ImportedTestCase(
        name=tc.get("name", "Test"),
        steps=steps,
        assertions=assertions,
        framework="ai",
        source_path=source_path,
    )


def extract_with_ai(path: Path, base_url: str = "") -> list[ImportedTestCase]:
    """Extract test cases from a file using the Claude Code CLI (`claude -p`).

    Uses the logged-in Claude Code session — no ANTHROPIC_API_KEY required.

    Raises:
        RuntimeError: If the `claude` CLI is not found or exits with an error.
        ValueError: If Claude returns unparseable JSON.
    """
    claude_bin = shutil.which("claude")
    if not claude_bin:
        raise RuntimeError(
            "Claude Code CLI not found on PATH. "
            "Install it from https://claude.ai/code and log in with `claude auth login`."
        )

    # Python reads the file — Claude never touches the filesystem
    content = path.read_text(encoding="utf-8", errors="ignore")
    if not content.strip():
        return []

    result = subprocess.run(
        [
            claude_bin, "-p", _build_instruction(path.name),
            "--output-format", "json",
            "--tools", "",
            "--no-session-persistence",
            "--system-prompt",
            (
                "You are a pure JSON extractor. "
                "Output ONLY a valid JSON array — no prose, no markdown headers, "
                "no explanations, no commentary before or after the JSON. "
                "If there is nothing to extract, output an empty array: []. "
                "Never ask questions. Never add any text outside the JSON array."
            ),
        ],
        input=content,          # file content piped via stdin
        capture_output=True,
        text=True,
        timeout=120,
    )

    if result.returncode != 0:
        raise RuntimeError(
            f"Claude Code exited with code {result.returncode}: {result.stderr[:300]}"
        )

    # claude -p --output-format json returns an envelope: {"result": "<text>", ...}
    try:
        envelope = json.loads(result.stdout)
        raw = envelope.get("result", "").strip()
    except json.JSONDecodeError:
        raw = result.stdout.strip()

    # Strip markdown code fences if Claude wrapped the JSON
    if raw.startswith("```"):
        parts = raw.split("```", 2)
        inner = parts[1] if len(parts) > 1 else raw
        if inner.startswith("json"):
            inner = inner[4:]
        raw = inner.rsplit("```", 1)[0].strip()

    # If raw starts with prose, try to locate the JSON array embedded in the text
    if not raw.startswith("["):
        # Look for a markdown-fenced JSON block anywhere in the text
        fence_match = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", raw, re.S)
        if fence_match:
            raw = fence_match.group(1).strip()
        else:
            # Find '[' that looks like a JSON array start: '[{', '["', or '[]'.
            # This avoids false-positives from CSS selectors like [placeholder="..."].
            json_array_match = re.search(r'\[\s*(?:\{|"|\'|\])', raw)
            if json_array_match:
                bracket_start = json_array_match.start()
                depth = 0
                for i in range(bracket_start, len(raw)):
                    if raw[i] == "[":
                        depth += 1
                    elif raw[i] == "]":
                        depth -= 1
                        if depth == 0:
                            raw = raw[bracket_start : i + 1]
                            break

    try:
        data: list[dict] = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Claude returned invalid JSON for {path.name}: {exc}\nRaw: {raw[:200]}") from exc

    return [_to_imported(tc, str(path), base_url) for tc in data if isinstance(tc, dict)]
