"""Generator for structured test step files (Mirabilis component format).

Each test case is written as a .json file containing an ordered array of
component-format steps.  These files are:
  1. Executed locally by maeris-mcp's in-process Playwright runner.
  2. Pushed to Mirabilis directly — no field translation needed.
"""

import json
import re
from pathlib import Path
from typing import Any


def generate_step_files(
    test_cases: list[dict],
    flow_name: str,
    base_url: str,
    output_dir: str,
    auth_config: dict | None = None,
    post_login_flow: list | None = None,
    credentials: dict | None = None,
) -> dict[str, Any]:
    """Write each test case as a JSON step file.

    Each test case dict must have:
        name  : str           — human-readable test name
        steps : list[dict]    — Mirabilis component-format step dicts

    Optional fields (stored but not used by the runner):
        description, priority, tags

    When auth_config is provided and a test does not already contain login
    steps, login steps are prepended automatically so that no test ever
    starts without authentication regardless of what the LLM generated.

    Returns a dict with generation metadata.
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    # Build the mandatory login prefix once (reused across all test cases)
    _login_prefix = _build_login_prefix(auth_config or {}, post_login_flow or [], base_url, credentials or {})

    files_generated: list[str] = []
    test_file_count = 0

    for idx, test_case in enumerate(test_cases, start=1):
        name = test_case.get("name") or f"test_{idx}"
        slug = _slugify(name)
        filename = f"{slug}.json"
        file_path = output_path / filename

        # Resolve {{BASE_URL}} placeholders that the LLM may have left in mock_input
        steps = _resolve_base_url(test_case.get("steps") or [], base_url)

        # Inject login prefix if the test doesn't already start with auth steps
        if _login_prefix and not _steps_have_login(steps):
            steps = _login_prefix + steps

        payload = {
            "name": name,
            "description": test_case.get("description", ""),
            "priority": test_case.get("priority", "medium"),
            "tags": test_case.get("tags") or [],
            "base_url": base_url or "",
            "steps": steps,
        }

        file_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
        files_generated.append(filename)
        test_file_count += 1

    return {
        "files_generated": files_generated,
        "test_files_count": test_file_count,
        "total_tests": test_file_count,
        "output_directory": output_dir,
    }


_LOGIN_KEYWORDS = re.compile(
    r"login|sign.?in|password|email|authenticate|credential",
    re.IGNORECASE,
)


def _steps_have_login(steps: list[dict]) -> bool:
    """Return True if steps already contain login/auth actions in the first 5 steps."""
    for step in steps[:5]:
        text = json.dumps(step)
        if _LOGIN_KEYWORDS.search(text):
            return True
    return False


def _build_login_prefix(
    auth_config: dict,
    post_login_flow: list,
    base_url: str,
    credentials: dict,
) -> list[dict]:
    """Build the mandatory login + post-login steps from scan_notes.auth.

    Returns an empty list if there is no auth config to work from.
    """
    if not isinstance(auth_config, dict):
        return []

    login_url = (auth_config.get("login_url") or "").strip()
    email_sel = (auth_config.get("email_selector") or '[type="email"]').strip()
    password_sel = (auth_config.get("password_selector") or '[type="password"]').strip()
    submit_sel = (auth_config.get("submit_selector") or 'button:has-text("Sign in")').strip()

    # No auth info at all → do not inject
    if not any([login_url, email_sel != '[type="email"]', submit_sel != 'button:has-text("Sign in")']):
        # Only skip if truly no auth detected
        if not auth_config.get("has_auth") and not auth_config.get("login_url"):
            return []

    # Resolve login URL to absolute
    if login_url and not login_url.startswith("http"):
        login_url = (base_url or "").rstrip("/") + "/" + login_url.lstrip("/")
    elif not login_url:
        login_url = (base_url or "").rstrip("/") + "/login"

    email = (credentials.get("email") or credentials.get("username") or "TEST_EMAIL").strip()
    password = (credentials.get("password") or "TEST_PASSWORD").strip()

    steps: list[dict] = [
        {
            "action_taken": "go_to",
            "mock_input": login_url,
            "description": "Navigate to login page",
        },
        {
            "css_selector": email_sel,
            "nth_appearance": 0,
            "action_taken": "send_keys",
            "mock_input": email,
            "description": "Enter email address",
        },
        {
            "css_selector": password_sel,
            "nth_appearance": 0,
            "action_taken": "send_keys",
            "mock_input": password,
            "description": "Enter password",
            "wait_after": 500,
        },
        {
            "css_selector": submit_sel,
            "nth_appearance": 0,
            "action_taken": "click",
            "description": "Click Sign in button",
            "wait_after": 2000,
        },
    ]

    # Inject post-login steps derived from post_login_flow scan notes
    post_steps = _build_post_login_steps(post_login_flow, base_url)
    steps.extend(post_steps)

    return steps


def _build_post_login_steps(post_login_flow: list, base_url: str) -> list[dict]:
    """Convert post_login_flow scan note entries into executable steps.

    Handles two patterns:
    - Step-like dicts already in the scan notes (passthrough)
    - String descriptions mentioning app/application selection → synthesized click step
    """
    if not post_login_flow or not isinstance(post_login_flow, list):
        return []

    steps: list[dict] = []
    app_selection_injected = False

    for item in post_login_flow:
        if isinstance(item, dict) and item.get("action_taken"):
            # Already a runnable step dict from scan notes
            steps.append(item)
            continue

        text = item if isinstance(item, str) else (
            " ".join(str(v) for v in item.values()) if isinstance(item, dict) else ""
        )
        text_lower = text.lower()

        if not app_selection_injected and (
            ("select" in text_lower or "choose" in text_lower or "pick" in text_lower)
            and ("app" in text_lower or "application" in text_lower or "workspace" in text_lower)
        ):
            # Synthesize an app selection click using domain hint from base_url
            app_hint = _extract_app_name_hint(base_url)
            css = f'button:has-text("{app_hint}")' if app_hint else "button"
            steps.append({
                "css_selector": css,
                "nth_appearance": 0,
                "action_taken": "click",
                "description": f"Select application{f' ({app_hint})' if app_hint else ''}",
                "wait_after": 1500,
            })
            app_selection_injected = True

    return steps


def _extract_app_name_hint(base_url: str) -> str:
    """Extract a short app name from base_url to use in button:has-text().

    E.g. https://light-dev.up.railway.app → 'light'
         https://myapp.staging.example.com → 'myapp'
         https://light-dev.up.railway.app             → ''  (can't determine)
    """
    if not base_url:
        return ""
    try:
        from urllib.parse import urlparse
        host = urlparse(base_url).hostname or ""
        if not host or host in ("localhost", "127.0.0.1"):
            return ""
        # Take the leftmost subdomain part, strip env suffixes like -dev/-staging/-prod
        first_part = host.split(".")[0]
        # Remove trailing -dev, -staging, -prod, -test, -local, -uat
        cleaned = re.sub(r"-(dev|staging|prod|test|local|uat|qa|demo|preview)$", "", first_part, flags=re.IGNORECASE)
        return cleaned if len(cleaned) >= 2 else first_part
    except Exception:
        return ""


def _resolve_base_url(steps: list[dict], base_url: str) -> list[dict]:
    """Replace {{BASE_URL}} in mock_input fields with the real base URL."""
    if not base_url:
        return steps
    resolved = []
    for step in steps:
        s = dict(step)
        if s.get("mock_input") and "{{BASE_URL}}" in s["mock_input"]:
            s["mock_input"] = s["mock_input"].replace("{{BASE_URL}}", base_url)
        resolved.append(s)
    return resolved


def _slugify(value: str) -> str:
    """Convert a string to a safe filename (lowercase, underscored)."""
    cleaned = "".join(ch.lower() if ch.isalnum() or ch.isspace() else "_" for ch in value)
    parts = [p for p in cleaned.split() if p]
    return "_".join(parts) or "test"
