"""Form test scenarios — sample values a form's designer saves with the form
(FormSettings.test_scenarios) and what should happen to them: accepted or
refused, which fields complain, what the formulas come to. Each scenario
runs through exactly the checks a real submission goes through (app/routers/
form_submissions.py: integrity, required fields, validation rules, formulas)
without saving anything — from the Form Designer, or for every form at once
from scripts/run_form_scenarios.py in CI.

URL parameters and session values can't be judged on the server (see
app/core/form_validation.py), so a scenario can set the user's role names
(for HASROLE and "show when") but nothing browser-only."""

import math
import uuid
from dataclasses import dataclass, field
from typing import Any

from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.form_validation import missing_required_fields, validate_field_values
from app.crud.form_integrity import check_submission_values
from app.crud.formula import compute_formula_fields


@dataclass
class ScenarioResult:
    name: str
    passed: bool
    failures: list[str] = field(default_factory=list)
    errors: dict[str, str] = field(default_factory=dict)  # what the checks said, by field
    values: dict[str, Any] = field(default_factory=dict)  # stored values incl. formulas


def _same(actual: Any, expected: Any) -> bool:
    if isinstance(actual, bool) or isinstance(expected, bool):
        return actual == expected
    if isinstance(actual, (int, float)) and isinstance(expected, (int, float)):
        return math.isclose(actual, expected, rel_tol=1e-9, abs_tol=1e-9)
    return actual == expected


def _detail_errors(detail: Any) -> dict[str, str]:
    """A 422's per-field messages, by field name."""
    if isinstance(detail, list):
        return {str(d.get("loc", ["?"])[-1]): str(d.get("msg", "")) for d in detail if isinstance(d, dict)}
    if isinstance(detail, dict) and "missing_required_fields" in detail:
        return {n: "is required" for n in detail["missing_required_fields"]}
    return {"form": str(detail)}


async def run_scenario(db: AsyncSession, tenant_id: uuid.UUID, field_defs: list[dict], scenario: dict) -> ScenarioResult:
    labels = {f.get("name"): f.get("label") or f.get("name") for f in field_defs}
    errors: dict[str, str] = {}
    given = scenario.get("values") or {}
    try:
        values = await check_submission_values(db, tenant_id, None, field_defs, given)
    except HTTPException as exc:
        errors.update(_detail_errors(exc.detail))
        values = {k: v for k, v in given.items() if k not in errors}
    scope = {"user": {"id": "", "name": "Test user", "email": "", "vendor_id": "", "tenant_id": str(tenant_id), "roles": list(scenario.get("roles") or [])}}
    for name in missing_required_fields(field_defs, values, scope):
        errors.setdefault(name, f"{labels.get(name, name)} is required.")
    for name, message in validate_field_values(field_defs, values, ctx=scope).items():
        errors.setdefault(name, message)
    formula_names = {f["name"] for f in field_defs if f.get("type") == "formula"}
    clean = {k: v for k, v in values.items() if k not in formula_names}
    stored = {**clean, **compute_formula_fields(field_defs, clean, scope)}

    failures: list[str] = []
    expect_valid = scenario.get("expect_valid", True)
    if expect_valid and errors:
        failures.append("Expected the form to be accepted, but it was refused: " + "; ".join(f"{labels.get(k, k)}: {v}" for k, v in errors.items()))
    if not expect_valid and not errors:
        failures.append("Expected the form to be refused, but it was accepted.")
    for name, text in (scenario.get("expect_errors") or {}).items():
        if name not in errors:
            failures.append(f"Expected a problem with {labels.get(name, name)}, but there was none.")
        elif text and text.lower() not in errors[name].lower():
            failures.append(f"{labels.get(name, name)}: expected a message containing “{text}”, got “{errors[name]}”.")
    for name, expected in (scenario.get("expect_values") or {}).items():
        actual = stored.get(name)
        if not _same(actual, expected):
            failures.append(f"{labels.get(name, name)}: expected {expected!r}, got {actual!r}.")
    return ScenarioResult(scenario.get("name") or "Scenario", not failures, failures, errors, stored)


async def run_scenarios(db: AsyncSession, tenant_id: uuid.UUID, field_defs: list[dict], scenarios: list[dict]) -> list[ScenarioResult]:
    return [await run_scenario(db, tenant_id, field_defs, s) for s in scenarios]
