"""Satellite registry API (`/api/registry/*`) — ADR #014, SATELLITE-CONTRACT.md.

Covers the token gate (disabled/missing/wrong, and that a valid app SESSION
never substitutes for the bearer token), resolve + kind mapping, search
(substring, kind filter, LIKE-wildcard escaping, limit cap), create (201 via
the choke point + audit, 409 dedup w/ existing payload, allow_duplicate,
near_matches), the project + org-reference reads, and invalid-uuid handling.
"""

from __future__ import annotations

import uuid

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models import AuditLog, Organization, Party
from app.services.auth import session as auth_session
from tests.registry.conftest import AUTH, TOKEN

RANDOM = "11111111-2222-3333-4444-555555555555"


# ---------------------------------------------------------------- token gate


def test_disabled_when_token_unset(reg_client_no_token, reg_ids):
    resp = reg_client_no_token.get(
        f"/api/registry/parties/{reg_ids.brentwood}", headers=AUTH
    )
    assert resp.status_code == 503
    assert resp.json()["error_code"] == "REGISTRY_DISABLED"


def test_missing_token_401(reg_client, reg_ids):
    resp = reg_client.get(f"/api/registry/parties/{reg_ids.brentwood}")
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "REGISTRY_AUTH_REQUIRED"


def test_wrong_token_401(reg_client, reg_ids):
    resp = reg_client.get(
        f"/api/registry/parties/{reg_ids.brentwood}",
        headers={"Authorization": "Bearer not-the-real-token"},
    )
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "REGISTRY_AUTH_INVALID"


def test_non_bearer_scheme_401(reg_client, reg_ids):
    resp = reg_client.get(
        f"/api/registry/parties/{reg_ids.brentwood}",
        headers={"Authorization": TOKEN},  # no "Bearer " prefix
    )
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "REGISTRY_AUTH_REQUIRED"


def test_valid_session_does_not_substitute_for_token(reg_client, reg_ids):
    """A live app-session cookie must NOT open a registry route — the
    service token is the ONLY accepted credential here (ADR #014)."""
    token = auth_session.mint_session(
        username="rian", email="rian@rian.ca", is_super_admin=True
    )
    reg_client.cookies.set(auth_session.SESSION_COOKIE, token)
    resp = reg_client.get(f"/api/registry/parties/{reg_ids.brentwood}")  # no bearer
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "REGISTRY_AUTH_REQUIRED"


def test_error_body_never_echoes_the_token(reg_client, reg_ids):
    resp = reg_client.get(
        f"/api/registry/parties/{reg_ids.brentwood}",
        headers={"Authorization": f"Bearer {TOKEN}-wrong"},
    )
    assert TOKEN not in resp.text


# ---------------------------------------------------------------- resolve


def test_resolve_org_kind_mapping(reg_client, reg_ids):
    resp = reg_client.get(f"/api/registry/parties/{reg_ids.brentwood}", headers=AUTH)
    assert resp.status_code == 200
    body = resp.json()
    assert body == {
        "id": str(reg_ids.brentwood),
        "kind": "organization",  # DB 'org' -> API 'organization'
        "name": "Brentwood",
        "is_active": True,
    }


def test_resolve_person_kind_mapping(reg_client, reg_ids):
    body = reg_client.get(
        f"/api/registry/parties/{reg_ids.heather}", headers=AUTH
    ).json()
    assert body["kind"] == "person"
    assert body["name"] == "Heather Bowden"


def test_resolve_inactive_is_active_false(reg_client, reg_ids):
    body = reg_client.get(
        f"/api/registry/parties/{reg_ids.oldco}", headers=AUTH
    ).json()
    assert body["is_active"] is False
    assert body["kind"] == "organization"


def test_resolve_unknown_404(reg_client):
    resp = reg_client.get(f"/api/registry/parties/{RANDOM}", headers=AUTH)
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "PARTY_NOT_FOUND"


def test_resolve_invalid_uuid_is_clean_404(reg_client):
    resp = reg_client.get("/api/registry/parties/not-a-uuid", headers=AUTH)
    assert resp.status_code == 404  # opaque id: malformed == unknown, never 500
    assert resp.json()["error_code"] == "PARTY_NOT_FOUND"


# ---------------------------------------------------------------- search


def test_search_substring_case_insensitive(reg_client):
    results = reg_client.get(
        "/api/registry/parties?query=BRENT", headers=AUTH
    ).json()["results"]
    names = [r["name"] for r in results]
    assert "Brentwood" in names
    assert "Brentwood Bay Dental" in names
    assert "Google" not in names


def test_search_exact_match_sorts_first(reg_client):
    results = reg_client.get(
        "/api/registry/parties?query=brentwood", headers=AUTH
    ).json()["results"]
    # "Brentwood" is an exact (lowercased) match -> rank 0, ahead of the
    # longer "Brentwood Bay Dental" which only contains the term.
    assert results[0]["name"] == "Brentwood"


def test_search_kind_filter_person(reg_client):
    results = reg_client.get(
        "/api/registry/parties?query=&kind=person", headers=AUTH
    ).json()["results"]
    assert [r["name"] for r in results] == ["Heather Bowden"]
    assert all(r["kind"] == "person" for r in results)


def test_search_kind_filter_organization_excludes_people(reg_client):
    results = reg_client.get(
        "/api/registry/parties?kind=organization", headers=AUTH
    ).json()["results"]
    assert all(r["kind"] == "organization" for r in results)
    assert "Heather Bowden" not in [r["name"] for r in results]
    # the inactive Old Co still shows, flagged
    oldco = next(r for r in results if r["name"] == "Old Co")
    assert oldco["is_active"] is False


def test_search_invalid_kind_422(reg_client):
    resp = reg_client.get("/api/registry/parties?kind=company", headers=AUTH)
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "BAD_KIND"


def test_search_escapes_like_wildcards(reg_client):
    # A literal '%' must not behave as "match everything".
    results = reg_client.get(
        "/api/registry/parties?query=%25", headers=AUTH  # %25 == '%'
    ).json()["results"]
    assert results == []


def test_search_escapes_underscore_wildcard(reg_client):
    # '_' is the single-char LIKE wildcard; escaped, it matches nothing here.
    results = reg_client.get(
        "/api/registry/parties?query=_", headers=AUTH
    ).json()["results"]
    assert results == []


def test_search_limit_cap(reg_engine, reg_client):
    engine, _ = reg_engine
    with Session(engine) as s:
        for n in range(60):
            s.add(Party(kind="org", name=f"Cap Test {n:02d}", slug=f"cap-{n:02d}"))
        s.commit()
    results = reg_client.get(
        "/api/registry/parties?query=cap%20test&limit=1000", headers=AUTH
    ).json()["results"]
    assert len(results) == 50  # MAX_SEARCH_LIMIT, even though 60 match


def test_search_limit_respected(reg_client):
    results = reg_client.get(
        "/api/registry/parties?query=brent&limit=1", headers=AUTH
    ).json()["results"]
    assert len(results) == 1


# ---------------------------------------------------------------- create


def test_create_org_201_choke_point_and_audit(reg_engine, reg_client):
    engine, _ = reg_engine
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "  New Client Inc  "}
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["kind"] == "organization"
    assert body["name"] == "New Client Inc"  # trimmed
    assert body["is_active"] is True
    assert body["near_matches"] == []
    new_id = uuid.UUID(body["id"])

    with Session(engine) as s:
        party = s.get(Party, new_id)
        assert party is not None and party.kind == "org"
        assert party.slug  # a slug was allocated
        # the 1:1 detail row exists (created via the choke point, ADR #013)
        assert s.get(Organization, new_id) is not None
        # exactly one audit row for the create, NULL tenant + NULL actor
        audit = s.scalars(
            select(AuditLog).where(AuditLog.action == "registry.create_org")
        ).all()
        assert len(audit) == 1
        row = audit[0]
        assert row.tenant_id is None
        assert row.actor_user_id is None
        assert row.entity_id == new_id
        assert row.after["via"] == "registry"
        assert row.after["name"] == "New Client Inc"


def test_create_org_slug_uniqueness(reg_engine, reg_client):
    # Two orgs, same name, allow_duplicate -> distinct parties + distinct slugs.
    first = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "Acme"}
    ).json()
    second = reg_client.post(
        "/api/registry/organizations",
        headers=AUTH,
        json={"name": "Acme", "allow_duplicate": True},
    ).json()
    assert first["id"] != second["id"]
    engine, _ = reg_engine
    with Session(engine) as s:
        slugs = s.scalars(
            select(Party.slug).where(func.lower(Party.name) == "acme")
        ).all()
        assert len(slugs) == len(set(slugs)) == 2


def test_create_org_exact_duplicate_409(reg_client, reg_ids):
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "Brentwood"}
    )
    assert resp.status_code == 409
    body = resp.json()
    assert body["error_code"] == "ORG_EXISTS"
    assert body["existing"]["id"] == str(reg_ids.brentwood)
    assert body["existing"]["kind"] == "organization"
    assert body["existing"]["name"] == "Brentwood"
    assert body["existing"]["is_active"] is True


def test_create_org_case_insensitive_duplicate_409(reg_client):
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "brentWOOD"}
    )
    assert resp.status_code == 409
    assert resp.json()["error_code"] == "ORG_EXISTS"


def test_create_org_allow_duplicate_201(reg_engine, reg_client):
    resp = reg_client.post(
        "/api/registry/organizations",
        headers=AUTH,
        json={"name": "Brentwood", "allow_duplicate": True},
    )
    assert resp.status_code == 201
    body = resp.json()
    # the original active Brentwood is a near-match (same name, either-dir substring)
    assert "Brentwood" in [m["name"] for m in body["near_matches"]]
    engine, _ = reg_engine
    with Session(engine) as s:
        count = s.scalar(
            select(func.count()).select_from(Party).where(
                Party.kind == "org", func.lower(Party.name) == "brentwood"
            )
        )
        assert count == 2


def test_create_org_near_matches_substring(reg_client):
    body = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "Brent"}
    ).json()
    assert body["kind"] == "organization"
    names = {m["name"] for m in body["near_matches"]}
    # both active orgs CONTAIN "brent"
    assert {"Brentwood", "Brentwood Bay Dental"} <= names
    # self is excluded, unrelated orgs are not matched
    assert "Brent" not in names
    assert "Google" not in names


def test_create_org_dedup_ignores_inactive(reg_engine, reg_client):
    # "Old Co" exists but is INACTIVE -> not a dedup clash; create succeeds.
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "Old Co"}
    )
    assert resp.status_code == 201


def test_create_org_blank_name_422(reg_client):
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "   "}
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "ORG_NAME_REQUIRED"


def test_create_org_too_long_name_422(reg_client):
    resp = reg_client.post(
        "/api/registry/organizations", headers=AUTH, json={"name": "x" * 201}
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "ORG_NAME_TOO_LONG"


def test_create_org_requires_token(reg_client):
    resp = reg_client.post("/api/registry/organizations", json={"name": "NoAuth"})
    assert resp.status_code == 401


# ---------------------------------------------------------------- projects


def test_get_project_200(reg_client, reg_ids):
    body = reg_client.get(
        f"/api/registry/projects/{reg_ids.project}", headers=AUTH
    ).json()
    assert body == {
        "id": str(reg_ids.project),
        "name": "Site Rebuild",
        "client_party_id": str(reg_ids.brentwood),
        "tenant_id": str(reg_ids.tenant),
    }


def test_get_project_404(reg_client):
    resp = reg_client.get(f"/api/registry/projects/{RANDOM}", headers=AUTH)
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "PROJECT_NOT_FOUND"


def test_get_project_invalid_uuid_404(reg_client):
    resp = reg_client.get("/api/registry/projects/nope", headers=AUTH)
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "PROJECT_NOT_FOUND"


# ---------------------------------------------------------------- org reference


def test_org_reference_200(reg_client, reg_ids):
    body = reg_client.get(
        f"/api/registry/organizations/{reg_ids.brentwood}/reference", headers=AUTH
    ).json()
    assert body == {
        "party_id": str(reg_ids.brentwood),
        "drive_folder_url": "https://drive.example/brentwood",
        "mosiah_folder": "/mnt/noah/central/Brentwood",
    }


def test_org_reference_unset_fields_are_null(reg_client, reg_ids):
    # Google has no reference facts set -> the fields are present but null.
    body = reg_client.get(
        f"/api/registry/organizations/{reg_ids.google}/reference", headers=AUTH
    ).json()
    assert body["party_id"] == str(reg_ids.google)
    assert body["drive_folder_url"] is None
    assert body["mosiah_folder"] is None


def test_org_reference_person_is_404(reg_client, reg_ids):
    # a person party is NOT an org -> 404 (never leaks a person as an org)
    resp = reg_client.get(
        f"/api/registry/organizations/{reg_ids.heather}/reference", headers=AUTH
    )
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "ORG_NOT_FOUND"


def test_org_reference_unknown_404(reg_client):
    resp = reg_client.get(
        f"/api/registry/organizations/{RANDOM}/reference", headers=AUTH
    )
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "ORG_NOT_FOUND"
