"""7G Forms anti-abuse: request gate, not field validation."""
from __future__ import annotations

import inspect
from datetime import UTC, datetime, timedelta
from pathlib import Path

from src.domain.connectors.invoke_capabilities import (
    CAPABILITY_CONVERSION_PATTERN,
    FORMS_FAMILY_PLAN,
    INVOKE_CAPABILITIES,
)
from src.domain.forms.anti_abuse import (
    CHALLENGE_FAILED,
    CHALLENGE_MISSING,
    CHALLENGE_UNAVAILABLE,
    FORMS_ANTI_ABUSE,
    HONEYPOT_TRIGGERED,
    RATE_LIMITED,
    ChallengeVerificationResult,
    FormAntiAbuseContext,
    FormAntiAbusePolicy,
    FormAttemptLimiter,
    InMemoryAttemptLimiter,
    StaticChallengeVerifier,
    UnavailableAttemptLimiter,
    UnavailableChallengeVerifier,
    evaluate_form_anti_abuse,
    process_form_submission_request,
)
from src.domain.forms.canonical import (
    FormDefinition,
    FormField,
    FormSubmission,
    contact_block_compatible_definition,
    quote_request_block_compatible_definition,
)
from src.domain.forms.files import FormFileRecord, InMemoryFormFileDirectory
from src.domain.forms.persistence import InMemoryFormSubmissionStore
from src.domain.forms.runtime import accept_form_submission
from src.domain.forms.validation import validate_form_values

_FIXED_AT = datetime(2026, 9, 13, 18, 0, tzinfo=UTC)
_FSB_A = "fsb_" + ("a" * 32)
_FILE_1 = "file_" + ("1" * 32)
_PASS = StaticChallengeVerifier(ChallengeVerificationResult(passed=True))
_FAIL = StaticChallengeVerifier(
    ChallengeVerificationResult(passed=False, failure_code=CHALLENGE_FAILED)
)


class CountingDirectory(InMemoryFormFileDirectory):
    def __init__(self) -> None:
        super().__init__()
        self.gets = 0

    def get(self, project_id: str, file_id: str):
        self.gets += 1
        return super().get(project_id, file_id)


class CountingLimiter:
    def __init__(self, inner: FormAttemptLimiter) -> None:
        self.inner = inner
        self.hits = 0

    def check_and_count(self, bucket_key: str, **kwargs):
        self.hits += 1
        return self.inner.check_and_count(bucket_key, **kwargs)


def _contact_values(**extra: object) -> dict:
    payload: dict = {
        "name": "Ada",
        "email": "ada@example.com",
        "message": "Hello",
    }
    payload.update(extra)
    return payload


def _process(
    raw: dict,
    *,
    policy: FormAntiAbusePolicy | None = None,
    verifier=None,
    limiter=None,
    files=None,
    definition=None,
    project_id: str = "prj_a",
    client_identity: str = "net_a",
    clock=None,
):
    return process_form_submission_request(
        definition or contact_block_compatible_definition(),
        raw,
        policy=policy or FormAntiAbusePolicy(),
        project_id=project_id,
        client_identity=client_identity,
        verifier=verifier or _PASS,
        limiter=limiter or InMemoryAttemptLimiter(),
        files=files,
        clock=clock or (lambda: _FIXED_AT),
        submission_id_factory=lambda: _FSB_A,
    )


def test_7g_anti_abuse_frozen() -> None:
    assert FORMS_ANTI_ABUSE["status"] == "frozen"
    assert FORMS_ANTI_ABUSE["execution_domain"] == "P"
    assert FORMS_ANTI_ABUSE["not_form_definition"] is True
    assert FORMS_ANTI_ABUSE["challenge_outage"] == "fail_closed"
    assert FORMS_ANTI_ABUSE["rate_limit_outage"] == "fail_closed"
    assert FORMS_ANTI_ABUSE["rate_limit_counts"] == "attempts"
    assert FORMS_ANTI_ABUSE["rate_limit_bucket"] == "project_form_client"
    assert FORMS_ANTI_ABUSE["submit_form_entry_migrated"] is False
    assert FORMS_ANTI_ABUSE["contact_adapter_migrated"] is False
    assert FORMS_ANTI_ABUSE["public_bff"] is False
    assert FORMS_ANTI_ABUSE["form_submit_capability"] is False
    assert "form.submit" not in INVOKE_CAPABILITIES
    assert CAPABILITY_CONVERSION_PATTERN["phase_7_status"] == "complete"
    assert CAPABILITY_CONVERSION_PATTERN["next_program_slice"] == (
        "9_app_block_modernization"
    )
    assert CAPABILITY_CONVERSION_PATTERN["opened_families"]["forms.*"] == "composition_integration"
    assert FORMS_FAMILY_PLAN["status"] == "complete"
    assert FORMS_FAMILY_PLAN["anti_abuse_frozen"] is True
    assert FORMS_FAMILY_PLAN["slices"][6] == "7g_anti_abuse"
    assert FORMS_FAMILY_PLAN["next_slice"] == "9_app_block_modernization"
    assert "turnstile_enabled" not in FormDefinition.model_fields
    assert "honeypot_field" not in FormDefinition.model_fields
    assert "ip_address" not in FormSubmission.model_fields
    params = inspect.signature(accept_form_submission).parameters
    assert "challenge_token" not in params
    assert "honeypot_value" not in params
    assert "client_identity" not in params


def test_blank_honeypot_passes_and_is_stripped_before_7c() -> None:
    result = _process(_contact_values(website=""))
    assert result.anti_abuse.allowed is True
    assert result.accepted is True
    values = result.runtime.submission.submission.values
    assert "website" not in values
    assert validate_form_values(
        contact_block_compatible_definition(),
        _contact_values(website=""),
    ).errors[0].code == "unknown_field"


def test_populated_honeypot_rejects_without_files_validation_or_id() -> None:
    files = CountingDirectory()
    files.put(FormFileRecord(project_id="prj_a", file_id=_FILE_1))
    limiter = CountingLimiter(InMemoryAttemptLimiter())
    definition = quote_request_block_compatible_definition()
    result = _process(
        {
            "name": "Ada",
            "email": "not-an-email",
            "message": "Hi",
            "website": "http://spam.test",
            "attachment": {"file_id": _FILE_1},
        },
        definition=definition,
        files=files,
        limiter=limiter,
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_FAIL,
    )
    assert result.anti_abuse.allowed is False
    assert result.anti_abuse.code == HONEYPOT_TRIGGERED
    assert result.runtime is None
    assert result.accepted is False
    assert files.gets == 0
    assert limiter.hits == 0


def test_honeypot_does_not_persist() -> None:
    store = InMemoryFormSubmissionStore()
    result = _process(_contact_values(website="bot"))
    assert result.anti_abuse.code == HONEYPOT_TRIGGERED
    assert result.runtime is None
    assert result.runtime is None or result.runtime.submission is None
    assert store.get("prj_a", _FSB_A) is None


def test_challenge_success_continues_without_provider_payload() -> None:
    result = _process(
        _contact_values(turnstile_token="tok_ok"),
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_PASS,
    )
    assert result.anti_abuse.allowed is True
    assert result.anti_abuse.code is None
    dumped = result.anti_abuse.model_dump()
    assert "cloudflare" not in str(dumped).lower()
    assert "hostname" not in dumped
    values = result.runtime.submission.submission.values
    assert "turnstile_token" not in values


def test_challenge_failure_is_generic_and_allocates_no_id() -> None:
    result = _process(
        _contact_values(turnstile_token="bad"),
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_FAIL,
    )
    assert result.anti_abuse.code == CHALLENGE_FAILED
    assert result.runtime is None
    assert result.anti_abuse.model_dump().get("error-codes") is None


def test_missing_challenge_when_required_is_not_trusted() -> None:
    result = _process(
        _contact_values(),
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_PASS,
    )
    assert result.anti_abuse.code == CHALLENGE_MISSING
    assert result.runtime is None


def test_optional_challenge_bypass_is_policy_not_absence() -> None:
    result = _process(
        _contact_values(),
        policy=FormAntiAbusePolicy(challenge_required=False),
        verifier=_FAIL,
    )
    assert result.anti_abuse.allowed is True
    assert result.accepted is True


def test_challenge_outage_fail_closed() -> None:
    result = _process(
        _contact_values(turnstile_token="tok"),
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=UnavailableChallengeVerifier(),
    )
    assert result.anti_abuse.code == CHALLENGE_UNAVAILABLE
    assert result.runtime is None
    assert "RuntimeError" not in str(result.anti_abuse.model_dump())


def test_rate_limiter_outage_fail_closed() -> None:
    result = _process(
        _contact_values(),
        limiter=UnavailableAttemptLimiter(),
    )
    assert result.anti_abuse.code == RATE_LIMITED
    assert result.runtime is None
    assert "Redis" not in str(result.anti_abuse.model_dump())


def test_rate_limit_counts_attempts_including_invalid_payloads() -> None:
    limiter = InMemoryAttemptLimiter()
    policy = FormAntiAbusePolicy(rate_limit_max_attempts=2, rate_limit_window_seconds=60)
    first = _process({"email": "bad"}, policy=policy, limiter=limiter)
    second = _process({"email": "also-bad"}, policy=policy, limiter=limiter)
    third = _process(_contact_values(), policy=policy, limiter=limiter)
    assert first.anti_abuse.allowed is True
    assert first.accepted is False
    assert second.anti_abuse.allowed is True
    assert third.anti_abuse.code == RATE_LIMITED
    assert third.runtime is None
    assert third.anti_abuse.retry_after_seconds is not None
    assert third.anti_abuse.retry_after_seconds >= 1


def test_rate_limit_isolates_projects_and_forms() -> None:
    limiter = InMemoryAttemptLimiter()
    policy = FormAntiAbusePolicy(rate_limit_max_attempts=1, rate_limit_window_seconds=60)
    a = _process(_contact_values(), policy=policy, limiter=limiter, project_id="prj_a")
    b = _process(_contact_values(), policy=policy, limiter=limiter, project_id="prj_b")
    a2 = _process(_contact_values(), policy=policy, limiter=limiter, project_id="prj_a")
    assert a.accepted is True
    assert b.accepted is True
    assert a2.anti_abuse.code == RATE_LIMITED
    other_form = FormDefinition(
        key="newsletter",
        fields=[FormField(key="email", type="email", required=True)],
    )
    newsletter = _process(
        {"email": "ada@example.com"},
        policy=policy,
        limiter=limiter,
        project_id="prj_a",
        definition=other_form,
    )
    assert newsletter.accepted is True


def test_rate_limit_window_uses_injectable_clock() -> None:
    limiter = InMemoryAttemptLimiter()
    policy = FormAntiAbusePolicy(rate_limit_max_attempts=1, rate_limit_window_seconds=60)
    now = {"t": _FIXED_AT}

    def clock() -> datetime:
        return now["t"]

    first = _process(_contact_values(), policy=policy, limiter=limiter, clock=clock)
    blocked = _process(_contact_values(), policy=policy, limiter=limiter, clock=clock)
    now["t"] = _FIXED_AT + timedelta(seconds=61)
    later = _process(_contact_values(), policy=policy, limiter=limiter, clock=clock)
    assert first.accepted is True
    assert blocked.anti_abuse.code == RATE_LIMITED
    assert later.accepted is True


def test_cf_turnstile_response_never_reaches_7c() -> None:
    result = _process(_contact_values(**{"cf-turnstile-response": "tok"}))
    assert result.accepted is True
    assert "cf-turnstile-response" not in result.runtime.submission.submission.values
    leaked = validate_form_values(
        contact_block_compatible_definition(),
        _contact_values(**{"cf-turnstile-response": "tok"}),
    )
    assert any(error.code == "unknown_field" for error in leaked.errors)


def test_anti_abuse_errors_are_not_field_errors() -> None:
    result = _process(
        _contact_values(turnstile_token="x"),
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_FAIL,
    )
    assert result.runtime is None
    assert result.anti_abuse.code == CHALLENGE_FAILED
    assert result.anti_abuse.code not in {"invalid_email", "required", "unknown_field"}


def test_wraps_file_orchestration_after_gate() -> None:
    files = CountingDirectory()
    files.put(FormFileRecord(project_id="prj_a", file_id=_FILE_1))
    result = _process(
        {
            "name": "Ada",
            "email": "ada@example.com",
            "message": "Quote",
            "attachment": {"file_id": _FILE_1},
            "website": "",
            "turnstile_token": "tok",
        },
        definition=quote_request_block_compatible_definition(),
        files=files,
        policy=FormAntiAbusePolicy(challenge_required=True),
        verifier=_PASS,
    )
    assert result.accepted is True
    assert files.gets == 1
    assert result.runtime.submission.submission_id == _FSB_A
    assert "website" not in result.runtime.submission.submission.values
    assert "turnstile_token" not in result.runtime.submission.submission.values


def test_file_orphan_is_not_deleted_on_abuse_reject() -> None:
    files = CountingDirectory()
    files.put(FormFileRecord(project_id="prj_a", file_id=_FILE_1))
    result = _process(
        {
            "name": "Ada",
            "email": "ada@example.com",
            "message": "Quote",
            "attachment": {"file_id": _FILE_1},
            "website": "http://bot",
        },
        definition=quote_request_block_compatible_definition(),
        files=files,
    )
    assert result.anti_abuse.code == HONEYPOT_TRIGGERED
    assert files.get("prj_a", _FILE_1) is not None
    assert files.gets == 1  # only this explicit get, not verify during process


def test_evaluate_does_not_import_content_rules() -> None:
    context = FormAntiAbuseContext(
        project_id="prj_a",
        form_key="contact",
        client_identity="net_a",
        honeypot_value="",
        challenge_token=None,
    )
    gate = evaluate_form_anti_abuse(
        FormAntiAbusePolicy(challenge_required=False, rate_limit_max_attempts=0),
        context,
        verifier=_FAIL,
        limiter=UnavailableAttemptLimiter(),
        clock=lambda: _FIXED_AT,
    )
    assert gate.allowed is True


def test_anti_abuse_module_stays_provider_neutral() -> None:
    source = Path("src/domain/forms/anti_abuse.py").read_text(encoding="utf-8")
    lowered = source.lower()
    for token in (
        "cloudflare",
        "site_key",
        "turnstile_secret",
        "cf_ray",
        "sqlalchemy",
        "src.domain.connectors",
        "src.domain.cloud",
        "src.integrations",
        "httpx",
        "challenges.cloudflare.com",
    ):
        assert token not in source and token not in lowered
    dumped = FormAntiAbusePolicy().model_dump()
    assert "cloudflare" not in dumped
    assert "captcha_provider" not in dumped
