"""Hub service tests: shapes + counts on the two-tenant fixture, the
cross-tenant guarantee, and the super-admin gate (403 for a non-super-
admin and for a super admin under view-as) on EVERY endpoint."""

from __future__ import annotations

import json

import pytest

from app.services import hub as svc
from app.services.errors import ServiceError
from tests.hub.conftest import non_super, super_admin, viewing_super

SERVICE_FNS = (svc.projects, svc.organizations, svc.people, svc.tenants, svc.accounts)


def _by(rows, key, value):
    return next(r for r in rows if r[key] == value)


# --------------------------------------------------------------- shapes


def test_projects_counts_cross_tenant_and_order(graph):
    session, ns = graph
    rows = svc.projects(session, super_admin())["rows"]
    assert len(rows) == 3

    # cross-tenant: rows span BOTH tenants (the whole point of the hub)
    assert {r["tenant"] for r in rows} == {"Bowden Works", "PlusROI"}

    # order: tenant then project name — App(BW), Website(BW), Growth(PlusROI)
    assert [r["name"] for r in rows] == ["App", "Website", "Growth"]

    website = _by(rows, "name", "Website")
    assert website["client"] == "Copernic"
    assert website["operator"] == "Bowden Works"
    assert website["tenant"] == "Bowden Works"
    assert website["status"] == "active"
    assert website["notes"] == "the redesign"
    assert website["created_at"] and website["updated_at"]
    assert website["worker_count"] == 2  # Adi + Rian, dup Adi collapsed
    assert website["invoice_count"] == 1  # one entry attached to one invoice
    # money-free by design (Hub v0): no income / billout_adjustment_* keys
    assert "income" not in website and "billout_adjustment_pct" not in website

    app = _by(rows, "name", "App")
    assert app["worker_count"] == 0 and app["invoice_count"] == 0

    growth = _by(rows, "name", "Growth")
    assert growth["tenant"] == "PlusROI" and growth["client"] == "Copernic"
    assert growth["worker_count"] == 1 and growth["invoice_count"] == 0


def test_organizations_shape(graph):
    session, ns = graph
    rows = svc.organizations(session, super_admin())["rows"]
    # every org party: the two tenants + the two client orgs
    assert [r["name"] for r in rows] == ["Acme", "Bowden Works", "Copernic", "PlusROI"]

    copernic = _by(rows, "name", "Copernic")
    assert copernic["is_tenant"] is False
    assert copernic["project_count"] == 2  # client of Website + Growth
    assert copernic["member_count"] == 1  # Heather affiliated
    # party fields
    assert copernic["email"] == "hello@copernic.com"
    assert copernic["phone"] == "555-0100"
    assert copernic["address"] == "1 Copernic Way"
    assert copernic["notes"] == "client since 2024"
    assert copernic["is_active"] is True
    assert copernic["created_at"] and copernic["updated_at"]
    # organizations detail-row fields
    assert copernic["legal_name"] == "Copernic Inc."
    assert copernic["currency"] == "CAD"
    assert copernic["drive_folder_url"] == "https://drive.example/copernic"
    assert copernic["mosiah_folder"] == "/mosiah/copernic"

    bw = _by(rows, "name", "Bowden Works")
    assert bw["is_tenant"] is True
    assert bw["project_count"] == 0 and bw["member_count"] == 0

    assert _by(rows, "name", "Acme")["project_count"] == 1
    # Acme has NO organizations detail row (the 1:1 invariant violated) —
    # the LEFT JOIN must survive it and surface nulls, not crash or drop it.
    acme = _by(rows, "name", "Acme")
    assert acme["legal_name"] is None
    assert acme["currency"] is None
    assert acme["email"] is None


def test_organization_detail_shape(graph):
    session, ns = graph
    detail = svc.organization_detail(session, super_admin(), ns.copernic.id)

    # Copernic is a client of BOTH tenants — one client_profiles row per
    # tenant, with DIFFERENT terms. This is the multi-tenant signal the
    # drill-down exists to surface, not a bug.
    profiles = detail["client_profiles"]
    assert [p["tenant"] for p in profiles] == ["Bowden Works", "PlusROI"]

    bw_profile = _by(profiles, "tenant", "Bowden Works")
    assert bw_profile["currency"] == "CAD"
    assert bw_profile["terms"] == "net30"
    assert bw_profile["default_commission_pct"] is None
    assert bw_profile["credit_limit"] == 1000
    assert bw_profile["contacts"] == "ops@copernic.com | billing@copernic.com"
    assert bw_profile["archived"] is False
    assert bw_profile["created_at"] and bw_profile["updated_at"]

    plus_profile = _by(profiles, "tenant", "PlusROI")
    assert plus_profile["currency"] == "USD"
    assert plus_profile["terms"] == "net15"
    assert plus_profile["default_commission_pct"] == 10
    assert plus_profile["commission_party"] == "Rian Owner"
    assert plus_profile["credit_limit"] == 5000

    # Copernic has no tenant_vendors rows — Acme does (a different org).
    assert detail["tenant_vendors"] == []

    acme_detail = svc.organization_detail(session, super_admin(), ns.acme.id)
    assert acme_detail["client_profiles"] == []
    vendors = acme_detail["tenant_vendors"]
    assert len(vendors) == 1
    assert vendors[0]["tenant"] == "Bowden Works"
    assert vendors[0]["default_category"] == "software"
    assert vendors[0]["default_paid_by"] == "Bowden Works"


def test_organization_detail_empty_for_org_with_no_tenant_relationships(graph):
    session, ns = graph
    # Heather has no client_profiles/tenant_vendors of her own (she's a
    # person, but even a plain org with none should just come back empty).
    detail = svc.organization_detail(session, super_admin(), ns.heather_p.id)
    assert detail == {"client_profiles": [], "tenant_vendors": []}


@pytest.mark.parametrize("actor_fn", [non_super, viewing_super])
def test_organization_detail_forbidden(graph, actor_fn):
    session, ns = graph
    with pytest.raises(ServiceError) as err:
        svc.organization_detail(session, actor_fn(), ns.copernic.id)
    assert err.value.status == 403 and err.value.code == "FORBIDDEN"


def test_people_shape(graph):
    session, ns = graph
    rows = svc.people(session, super_admin())["rows"]
    assert [r["name"] for r in rows] == ["Adi Worker", "Danielle", "Heather", "Rian Owner"]

    adi = _by(rows, "name", "Adi Worker")
    assert adi["has_account"] is True
    assert adi["is_worker"] is True
    assert adi["affiliation_count"] == 0

    heather = _by(rows, "name", "Heather")
    assert heather["has_account"] is False  # no users row
    assert heather["is_worker"] is False  # only an ENDED engagement
    assert heather["affiliation_count"] == 1
    assert heather["slug"] == "heather"
    assert heather["is_active"] is True
    assert heather["created_at"] and heather["updated_at"]


def test_tenants_shape(graph):
    session, ns = graph
    rows = svc.tenants(session, super_admin())["rows"]
    assert [r["name"] for r in rows] == ["Bowden Works", "PlusROI"]

    bw = _by(rows, "name", "Bowden Works")
    assert bw["slug"] == "bowden-works"
    assert bw["books_owner"] is None
    assert bw["settings"] == "{}"
    # distinct PEOPLE with an active engagement: Rian (owner) + Adi (worker).
    # The Copernic direct_client (org party_b) and Heather (ended) are excluded.
    assert bw["member_count"] == 2
    assert bw["project_count"] == 2

    plus = _by(rows, "name", "PlusROI")
    assert plus["slug"] == "plusroi"
    assert plus["books_owner"] == "app"
    # the raw settings blob carries EVERY key, not just the derived one
    assert json.loads(plus["settings"]) == {"books_owner": "app", "parallel_run": False}
    assert plus["member_count"] == 1  # Danielle
    assert plus["project_count"] == 1


def test_accounts_shape(graph):
    session, ns = graph
    rows = svc.accounts(session, super_admin())["rows"]
    assert [r["idauth_username"] for r in rows] == ["adi", "dani", "rian"]

    rian = _by(rows, "idauth_username", "rian")
    assert rian["person_name"] == "Rian Owner"
    assert rian["person_id"] == str(ns.rian_p.id)
    assert rian["is_super_admin"] is True
    assert rian["tenant_memberships"] == ["bowden-works"]
    assert rian["id"] and rian["created_at"]
    assert rian["preferences"] == "{}"

    adi = _by(rows, "idauth_username", "adi")
    assert adi["is_super_admin"] is False
    assert adi["tenant_memberships"] == ["bowden-works"]

    assert _by(rows, "idauth_username", "dani")["tenant_memberships"] == ["plusroi"]


# --------------------------------------------------------------- the gate


@pytest.mark.parametrize("fn", SERVICE_FNS)
def test_non_super_admin_forbidden_every_endpoint(graph, fn):
    session, ns = graph
    with pytest.raises(ServiceError) as err:
        fn(session, non_super())
    assert err.value.status == 403 and err.value.code == "FORBIDDEN"


@pytest.mark.parametrize("fn", SERVICE_FNS)
def test_viewing_as_forbidden_every_endpoint(graph, fn):
    """Even a super admin is refused while VIEWING-AS someone — the gate is
    is_super_admin && !is_viewing_as."""
    session, ns = graph
    with pytest.raises(ServiceError) as err:
        fn(session, viewing_super())
    assert err.value.status == 403 and err.value.code == "FORBIDDEN"
