"""8E: missing required vars fail render; optional vars use catalog defaults."""
from __future__ import annotations

import json
import re
from pathlib import Path

import pytest
from pydantic import ValidationError

from src.domain.codegen.email_templates_manifest import (
    EMAIL_TEMPLATE_CATALOG,
    OVERLAY_SYNC_CONTRACT,
    EmailTemplateDeclaration,
    declared_variable_names,
    unknown_placeholders,
)
from src.domain.connectors.invoke_capabilities import CAPABILITY_CONVERSION_PATTERN, FORMS_FAMILY_PLAN
from src.domain.connectors.schemas import InvokeEmailSendRequest
from src.domain.forms.email import FORBIDDEN_EMAIL_CONSTANT_KEYS, FORMS_EMAIL_COMPOSITION
from tests.email_catalog_test_support import load_cloud_catalog_meta

BE_ROOT = Path(__file__).resolve().parents[1]
CLOUD_LIB = BE_ROOT.parent / "P2HSCloud" / "apps" / "pc-gateway" / "src" / "lib"
CLOUD_RENDER = CLOUD_LIB / "email-templates.ts"
CLOUD_VARS = CLOUD_LIB / "email-template-vars.ts"
CLOUD_VALIDATE = CLOUD_LIB / "email-template-validate.ts"
CLOUD_ROUTE = BE_ROOT.parent / "P2HSCloud" / "apps" / "pc-gateway" / "src" / "routes" / "email-templates.ts"
AUTH_TS = BE_ROOT.parent / "P2HSCloud" / "apps" / "pc-gateway" / "src" / "routes" / "auth.ts"
NOTIFY_YAML = BE_ROOT / "seeds" / "function_library" / "contact-capture-notify.yaml"
CORE_META = BE_ROOT / "src" / "domain" / "codegen" / "email_template_catalog.meta.json"

CLOSED_KEYS = (
    "auth.verify_email",
    "auth.otp_request",
    "auth.magic_link",
    "auth.password_reset",
    "auth.invite",
    "contact_capture.admin_notify",
    "contact_capture.confirmation",
)

AUTH_KEYS = CLOSED_KEYS[:5]


def prepare_render_vars(entry: dict, vars_map: dict[str, str]) -> dict:
    missing = sorted(
        name
        for name in entry["required_variables"]
        if not isinstance(vars_map.get(name), str) or vars_map[name] == ""
    )
    if missing:
        return {"ok": False, "code": "missing_required_variable", "missing_variables": missing}
    resolved = dict(vars_map)
    for name, fallback in (entry.get("optional_variables") or {}).items():
        value = vars_map.get(name)
        if not isinstance(value, str) or value == "":
            resolved[name] = fallback
    return {"ok": True, "vars": resolved}


def test_seven_key_variable_inventory_parity() -> None:
    core = json.loads(CORE_META.read_text(encoding="utf-8"))
    cloud = load_cloud_catalog_meta()
    assert tuple(core) == tuple(cloud) == CLOSED_KEYS
    expected = {
        "auth.verify_email": (["action_url"], {"app_name": "Your application"}),
        "auth.otp_request": (["code"], {"app_name": "Your application"}),
        "auth.magic_link": (["action_url"], {"app_name": "Your application"}),
        "auth.password_reset": (["action_url"], {"app_name": "Your application"}),
        "auth.invite": (["action_url"], {"app_name": "Your application"}),
        "contact_capture.admin_notify": ([], {"name": "a visitor", "email": "", "message": ""}),
        "contact_capture.confirmation": ([], {"name": "there"}),
    }
    for key in CLOSED_KEYS:
        req, opt = expected[key]
        assert EMAIL_TEMPLATE_CATALOG[key]["required_variables"] == req == cloud[key]["requiredVariables"] == core[key]["required_variables"]
        assert dict(EMAIL_TEMPLATE_CATALOG[key]["optional_variables"]) == opt == cloud[key]["optionalVariables"] == core[key]["optional_variables"]
        assert set(EMAIL_TEMPLATE_CATALOG[key]["protected_placeholders"]) <= {
            f"{{{{{name}}}}}" for name in req
        }


@pytest.mark.parametrize("key", AUTH_KEYS)
def test_required_variable_missing_fails(key: str) -> None:
    entry = EMAIL_TEMPLATE_CATALOG[key]
    required = entry["required_variables"][0]
    result = prepare_render_vars(entry, {})
    assert result["ok"] is False
    assert result["code"] == "missing_required_variable"
    assert result["missing_variables"] == [required]
    supplied = prepare_render_vars(entry, {required: "value"})
    assert supplied["ok"] is True
    empty = prepare_render_vars(entry, {required: ""})
    assert empty["ok"] is False
    assert empty["missing_variables"] == [required]


def test_multiple_missing_required_reported_together() -> None:
    fake = {"required_variables": ["action_url", "code"], "optional_variables": {}}
    result = prepare_render_vars(fake, {})
    assert result["missing_variables"] == ["action_url", "code"]


def test_optional_defaults_and_empty_string() -> None:
    admin = EMAIL_TEMPLATE_CATALOG["contact_capture.admin_notify"]
    missing = prepare_render_vars(admin, {})
    assert missing["ok"] is True
    assert missing["vars"]["name"] == "a visitor"
    assert missing["vars"]["email"] == ""
    assert missing["vars"]["message"] == ""
    empty_name = prepare_render_vars(admin, {"name": "", "email": "a@b.co", "message": "Hi"})
    assert empty_name["vars"]["name"] == "a visitor"
    assert empty_name["vars"]["email"] == "a@b.co"
    supplied = prepare_render_vars(admin, {"name": "Ada", "email": "a@b.co", "message": ""})
    assert supplied["vars"]["name"] == "Ada"
    assert supplied["vars"]["message"] == ""
    confirm = prepare_render_vars(EMAIL_TEMPLATE_CATALOG["contact_capture.confirmation"], {})
    assert confirm["vars"]["name"] == "there"
    confirm_name = prepare_render_vars(EMAIL_TEMPLATE_CATALOG["contact_capture.confirmation"], {"name": "Bo"})
    assert confirm_name["vars"]["name"] == "Bo"


def test_extra_input_variables_ignored() -> None:
    result = prepare_render_vars(
        EMAIL_TEMPLATE_CATALOG["auth.verify_email"],
        {"action_url": "https://example.com/v", "debug": "x"},
    )
    assert result["ok"] is True
    assert result["vars"]["debug"] == "x"
    assert result["vars"]["action_url"] == "https://example.com/v"


def test_non_string_inputs_are_dropped_then_missing() -> None:
    route = CLOUD_ROUTE.read_text(encoding="utf-8")
    assert "if (typeof value === \"string\") stringVars[key] = value" in route
    result = prepare_render_vars(EMAIL_TEMPLATE_CATALOG["auth.magic_link"], {})
    assert result["ok"] is False
    assert result["missing_variables"] == ["action_url"]


def test_unknown_placeholders_rejected_in_subject_text_html() -> None:
    with pytest.raises(ValidationError, match="unknown template variable"):
        EmailTemplateDeclaration(
            template_key="auth.otp_request",
            subject="Code {{typo_name}}",
            text_body="Use {{code}}",
        )
    with pytest.raises(ValidationError, match="unknown template variable"):
        EmailTemplateDeclaration(
            template_key="auth.otp_request",
            subject="Code",
            text_body="Use {{code}} {{typo_name}}",
        )
    with pytest.raises(ValidationError, match="unknown template variable"):
        EmailTemplateDeclaration(
            template_key="auth.otp_request",
            subject="Code",
            text_body="Use {{code}}",
            html_body="<p>{{code}}</p><span>{{typo_name}}</span>",
        )
    ok = EmailTemplateDeclaration(
        template_key="contact_capture.admin_notify",
        subject="From {{name}}",
        text_body="{{email}}\n{{message}}",
        html_body="<p>{{name}} {{email}} {{message}}</p>",
    )
    assert ok.template_key == "contact_capture.admin_notify"


def test_cloud_source_and_render_split() -> None:
    validate = CLOUD_VALIDATE.read_text(encoding="utf-8")
    render = CLOUD_RENDER.read_text(encoding="utf-8")
    vars_src = CLOUD_VARS.read_text(encoding="utf-8")
    assert "unknown_template_variable" in validate
    assert "missing_required_variable" in render
    assert "prepareRenderVars" in vars_src
    assert "usedDefault: true" in render
    assert "usedDefault: false" in render
    assert "missing_required_variable" in render
    route = CLOUD_ROUTE.read_text(encoding="utf-8")
    assert "missing_variables" in route
    assert "OTP" not in route
    assert "magic link" not in route.lower()


def test_auth_callers_supply_required_vars() -> None:
    auth = AUTH_TS.read_text(encoding="utf-8")
    assert "renderAuthEmailTemplate(env, projectId, \"auth.verify_email\", { action_url: verifyUrl })" in auth
    assert "renderAuthEmailTemplate(env, projectId, \"auth.otp_request\", { code })" in auth
    assert "renderAuthEmailTemplate(env, projectId, \"auth.magic_link\", { action_url: magicUrl })" in auth
    assert "renderAuthEmailTemplate(env, projectId, \"auth.password_reset\", { action_url: resetUrl })" in auth
    assert "renderAuthEmailTemplate(env, projectId, \"auth.invite\", { action_url: inviteUrl })" in auth
    assert "if (!rendered) return authEmailFailed()" in auth
    assert "if (!otpRendered) return authEmailFailed()" in auth
    assert "if (!magicRendered) return authEmailFailed()" in auth
    assert "if (!resetRendered) return authEmailFailed()" in auth
    assert "if (!inviteRendered) return authEmailFailed()" in auth


def test_contact_capture_caller_supplies_optional_raw_strings() -> None:
    notify = NOTIFY_YAML.read_text(encoding="utf-8")
    assert "name: submission.name || ''" in notify
    assert "email: submission.email || ''" in notify
    assert "message: submission.message || ''" in notify
    assert "if (!resp.ok) return fallback" in notify


def test_html_escaping_still_applied_after_defaults() -> None:
    render = CLOUD_RENDER.read_text(encoding="utf-8")
    assert "escapeHtml(v.action_url)" in render
    assert "escapeHtml(v.code)" in render
    assert "escapeHtml(v.name)" in render
    assert "substituteHtml(override.html_body, resolved)" in render
    assert "substituteText(override.subject, resolved)" in render


def test_successful_render_has_no_declared_unresolved_tokens() -> None:
    render = CLOUD_RENDER.read_text(encoding="utf-8")
    assert "assertNoDeclaredTokens" in render
    admin = prepare_render_vars(EMAIL_TEMPLATE_CATALOG["contact_capture.admin_notify"], {"name": "Ada"})
    assert "{{name}}" not in admin["vars"]["name"]
    names = declared_variable_names(EMAIL_TEMPLATE_CATALOG["auth.verify_email"])
    leftover = unknown_placeholders("Hello {{name}}", allowed=names)
    assert leftover == ["name"]


def test_defaults_and_overrides_share_variable_contract() -> None:
    render = CLOUD_RENDER.read_text(encoding="utf-8")
    assert "prepareRenderVars(meta, vars)" in render
    assert render.index("prepareRenderVars(meta, vars)") < render.index("def.defaultSubject(resolved)")
    assert render.index("prepareRenderVars(meta, vars)") < render.index("substituteText(override.subject, resolved)")


def test_email_send_forms_sync_unchanged() -> None:
    assert set(InvokeEmailSendRequest.model_fields) == {
        "to",
        "subject",
        "text",
        "html",
        "reply_to",
        "idempotency_key",
    }
    assert "template_key" not in InvokeEmailSendRequest.model_fields
    assert FORMS_EMAIL_COMPOSITION["email_templates"] is False
    assert FORMS_EMAIL_COMPOSITION["interpolation"] is False
    assert "template_key" in FORBIDDEN_EMAIL_CONSTANT_KEYS
    assert OVERLAY_SYNC_CONTRACT == "replace_all"


def test_tracker_advances_past_8e() -> None:
    assert CAPABILITY_CONVERSION_PATTERN["phase_8d_status"] == "complete"
    assert CAPABILITY_CONVERSION_PATTERN["phase_8e_status"] == "complete"
    assert CAPABILITY_CONVERSION_PATTERN["next_program_slice"] == "9_app_block_modernization"
    assert FORMS_FAMILY_PLAN["next_slice"] == "9_app_block_modernization"
    assert CAPABILITY_CONVERSION_PATTERN["phase_7_status"] == "complete"
