"""Deterministic runtime-invariants extractor: sticky, never resets a recorded value."""
import pytest

from src.domain.projects.runtime_invariants import (
    ProjectRuntimeInvariants,
    extract_runtime_invariants,
    validate_runtime_routes,
)


def test_defaults_match_todays_hardcoded_behavior():
    """Every existing project has runtime_invariants IS NULL / {} -- must reproduce
    exactly what ManifestAuthSection hardcodes today, so no existing project changes
    behavior just because this column now exists."""
    defaults = ProjectRuntimeInvariants.model_validate({})
    assert defaults.admin_panel_path == "/admin"
    assert defaults.customer_login_path == "/login"
    assert defaults.allow_custom_backend is False
    assert defaults.persistence_required is False
    assert defaults.auth_modes == []
    assert defaults.email_preference is None
    assert defaults.source == "default"


def test_named_admin_path_and_magic_link_locked_in_one_prompt():
    previous = ProjectRuntimeInvariants()
    prompt = (
        "Can we create a dashboard where we can see the contact form submissions? "
        "I would prefer a secure admin url like /be-admin and no index to that and "
        "only accessible with the magic link"
    )
    updated = extract_runtime_invariants(prompt, previous)
    assert updated.admin_panel_path == "/be-admin"
    assert "magic_link" in updated.auth_modes
    assert updated.source == "prompt"


def test_locked_admin_path_survives_an_unrelated_later_prompt():
    """The exact shape of today's real bug: a second, unrelated codegen run must not
    reset the admin path back to the default."""
    locked = ProjectRuntimeInvariants(admin_panel_path="/be-admin", source="prompt")
    later = extract_runtime_invariants(
        "Add a phone number to the header and footer.", locked
    )
    assert later.admin_panel_path == "/be-admin"
    assert later == locked  # nothing changed -> caller skips the DB write


def test_bare_admin_path_never_overwrites_a_locked_custom_path():
    locked = ProjectRuntimeInvariants(admin_panel_path="/be-admin", source="prompt")
    later = extract_runtime_invariants(
        "Make sure the admin dashboard at /admin still loads fast.", locked
    )
    assert later.admin_panel_path == "/be-admin"


def test_admin_login_wording_does_not_make_admin_route_customer_login():
    updated = extract_runtime_invariants(
        "Create a secure /admin panel with login authentication.",
        ProjectRuntimeInvariants(),
    )
    assert updated.admin_panel_path == "/admin"
    assert updated.customer_login_path == "/login"


def test_admin_and_customer_routes_must_be_distinct():
    assert validate_runtime_routes(ProjectRuntimeInvariants()) == []
    assert validate_runtime_routes(
        ProjectRuntimeInvariants(admin_panel_path="/admin", customer_login_path="/admin")
    ) == ["admin_panel_path_must_differ_from_customer_login_path"]


def test_extractor_rejects_route_collision():
    with pytest.raises(ValueError, match="admin_panel_path_must_differ"):
        extract_runtime_invariants(
            "Customers can sign in at /admin.",
            ProjectRuntimeInvariants(),
        )


def test_explicit_customer_login_route_is_extracted():
    updated = extract_runtime_invariants(
        "Customers can sign in at /signin.",
        ProjectRuntimeInvariants(),
    )
    assert updated.customer_login_path == "/signin"


def test_persistence_and_admin_read_requirement_inferred_and_sticky():
    previous = ProjectRuntimeInvariants()
    prompt = "Create a contact form and let the admin view submissions."
    updated = extract_runtime_invariants(prompt, previous)
    assert updated.persistence_required is True
    assert updated.admin_must_read_persisted is True
    assert updated.source == "prompt"

    later = extract_runtime_invariants("Change the header color to blue.", updated)
    assert later.persistence_required is True
    assert later.admin_must_read_persisted is True
    assert later == updated


def test_unsafe_path_is_ignored_not_recorded():
    previous = ProjectRuntimeInvariants()
    updated = extract_runtime_invariants(
        "Put the admin panel at /../etc/passwd please.", previous
    )
    assert updated.admin_panel_path == "/admin"
    assert updated == previous


def test_email_provider_keyword_recorded_once():
    previous = ProjectRuntimeInvariants()
    updated = extract_runtime_invariants(
        "Send contact notifications through Resend.", previous
    )
    assert updated.email_preference == "resend"

    later = extract_runtime_invariants(
        "Also try Mailgun for this.", updated
    )
    # first explicit choice wins; Phase 2's resolver (not this extractor) validates it
    assert later.email_preference == "resend"


def test_discovery_source_never_downgraded_to_prompt():
    from_discovery = ProjectRuntimeInvariants(
        admin_panel_path="/ops", source="discovery"
    )
    updated = extract_runtime_invariants(
        "Also send owner notifications via Postmark.", from_discovery
    )
    assert updated.email_preference == "postmark"
    assert updated.source == "discovery"
