"""Clients + Contacts write service (services/clients.py; phase-3 slice 1).

SQLite fixtures per the house style (mirrors tests/books/test_books.py):
an app-owned PlusROI tenant (writable), a second tenant to prove
isolation, a sheet-owned tenant to prove the write-block, and
SimpleNamespace actors for the owner/worker authz cells.

IDs on the wire are strings; ``pid()`` re-hydrates a UUID to pass back to
the service/session (FastAPI does this coercion in production).
"""

import uuid
from types import SimpleNamespace

import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.models import (
    Base,
    ClientProfile,
    Engagement,
    Organization,
    Party,
    PartyAffiliation,
    Tenant,
)
from app.services import clients as svc
from app.services.entities.parties import create_org_party
from app.services.entities.resolve import _allocate_slug
from app.services.errors import ServiceError


def pid(raw: str) -> uuid.UUID:
    return uuid.UUID(raw)


@pytest.fixture()
def s():
    engine = create_engine(
        "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
    )
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    engine.dispose()


def _tenant(s, name, slug, settings):
    party = Party(kind="org", name=name, slug=slug)
    s.add(party)
    s.flush()
    tenant = Tenant(party_id=party.id, settings=settings)
    s.add(tenant)
    s.flush()
    return tenant, party


APP = {"books_owner": "app", "default_currency": "CAD"}
SHEET = {"books_owner": "sheet"}


@pytest.fixture()
def fx(s):
    plusroi, plusroi_party = _tenant(s, "PlusROI", "plusroi", APP)
    other, other_party = _tenant(s, "Other Books", "other", APP)
    sheet, sheet_party = _tenant(s, "SheetCo", "sheetco", SHEET)
    s.flush()
    return SimpleNamespace(
        plusroi=plusroi,
        plusroi_party=plusroi_party,
        other=other,
        other_party=other_party,
        sheet=sheet,
        sheet_party=sheet_party,
    )


def owner(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: True,
    )


def worker(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: False,
    )


def _org_count(s):
    return s.scalar(select(func.count()).select_from(Party).where(Party.kind == "org"))


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


def test_create_client_full_roundtrip(s, fx):
    res = svc.create_client(
        s,
        owner(fx.plusroi.id),
        {
            "name": "Copernic",
            "official_name": "Copernic Inc.",
            "address": "1 King St",
            "phone": "555-1000",
            "currency": "USD",
            "terms": "Net 30",
            "invoice_timing": "month_end",
            "credit_limit": "5000",
            "default_commission_pct": "10",
            "archived": False,
            "contacts": [
                {"name": "Ada Client", "email": "ada@copernic.com"},
                {"name": "Bo Client", "email": "bo@copernic.com"},
            ],
        },
    )
    assert res["ok"] is True and res["reused_party"] is False
    client_id = pid(res["row"]["party_id"])
    assert res["row"]["currency"] == "USD"
    assert res["row"]["official_name"] == "Copernic Inc."
    assert res["row"]["contact_count"] == 2
    assert res["row"]["credit_limit"] == "5000.00"

    # org party + 1:1 organizations detail with legal_name
    party = s.get(Party, client_id)
    assert party.kind == "org" and party.address == "1 King St"
    org = s.get(Organization, client_id)
    assert org.legal_name == "Copernic Inc." and org.currency == "USD"

    # client_profiles row for THIS tenant
    profile = s.get(ClientProfile, (fx.plusroi.id, client_id))
    assert profile is not None and profile.terms == "Net 30"
    assert str(profile.default_commission_pct) == "10.0000"

    # a direct_client engagement operator=tenant party, client=new org
    edge = s.scalar(
        select(Engagement).where(
            Engagement.tenant_id == fx.plusroi.id,
            Engagement.type == "direct_client",
            Engagement.party_a_id == fx.plusroi_party.id,
            Engagement.party_b_id == client_id,
        )
    )
    assert edge is not None and edge.role_a == "operator" and edge.role_b == "client"

    # contacts are cc_recipient affiliations to the client org
    affs = s.scalars(
        select(PartyAffiliation).where(PartyAffiliation.org_party_id == client_id)
    ).all()
    assert len(affs) == 2
    assert all(a.role == "cc_recipient" and a.ended_on is None for a in affs)

    # get_client round-trips the profile + contacts
    detail = svc.get_client(s, owner(fx.plusroi.id), client_id)
    assert detail["client"]["contact_count"] == 2
    assert {c["email"] for c in detail["contacts"]} == {
        "ada@copernic.com",
        "bo@copernic.com",
    }


def test_reuse_existing_org_no_dup_party(s, fx):
    existing = create_org_party(s, name="Copernic", slug=_allocate_slug(s, "Copernic"))
    s.commit()
    before = _org_count(s)

    res = svc.create_client(s, owner(fx.plusroi.id), {"name": "copernic"})
    assert res["reused_party"] is True
    assert res["row"]["party_id"] == str(existing.id)
    # NO new party minted — the same org gains a profile + edge
    assert _org_count(s) == before
    assert s.get(ClientProfile, (fx.plusroi.id, existing.id)) is not None
    assert (
        s.scalar(
            select(func.count())
            .select_from(Engagement)
            .where(
                Engagement.type == "direct_client",
                Engagement.party_b_id == existing.id,
            )
        )
        == 1
    )


def test_create_client_already_client_conflicts(s, fx):
    svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})
    with pytest.raises(ServiceError) as err:
        svc.create_client(s, owner(fx.plusroi.id), {"name": "copernic"})
    assert err.value.code == "CLIENT_EXISTS" and err.value.status == 409


def test_blank_contact_rows_ignored(s, fx):
    res = svc.create_client(
        s,
        owner(fx.plusroi.id),
        {"name": "Copernic", "contacts": [{"name": "", "email": ""}]},
    )
    assert res["row"]["contact_count"] == 0


# --------------------------------------------------------------- authz gates


@pytest.mark.parametrize(
    "call",
    [
        lambda a, s: svc.list_clients(s, a),
        lambda a, s: svc.create_client(s, a, {"name": "X"}),
    ],
)
def test_owner_gate_403(s, fx, call):
    with pytest.raises(ServiceError) as err:
        call(worker(fx.plusroi.id), s)
    assert err.value.code == "NOT_ALLOWED" and err.value.status == 403


def test_sheet_owned_blocks_every_mutation(s, fx):
    # the write-block fires FIRST, before any row load.
    o = owner(fx.sheet.id)
    fake = uuid.uuid4()
    for call in (
        lambda: svc.create_client(s, o, {"name": "X"}),
        lambda: svc.update_client(s, o, fake, {"name": "Y"}),
        lambda: svc.add_contact(s, o, fake, {"name": "A", "email": "a@b.com"}),
        lambda: svc.remove_contact(s, o, fake),
    ):
        with pytest.raises(ServiceError) as err:
            call()
        assert err.value.code == "SHEET_OWNED_READ_ONLY"
        assert err.value.status == 409


def test_tenant_isolation(s, fx):
    res = svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})
    client_id = res["row"]["party_id"]

    # the other tenant never sees it in the list…
    other_rows = svc.list_clients(s, owner(fx.other.id))["rows"]
    assert client_id not in {r["party_id"] for r in other_rows}

    # …and a by-id read is a clean 404 (not another tenant's data)
    with pytest.raises(ServiceError) as err:
        svc.get_client(s, owner(fx.other.id), pid(client_id))
    assert err.value.code == "CLIENT_NOT_FOUND" and err.value.status == 404


# --------------------------------------------------------------- validation


def test_validation_name_required(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_client(s, owner(fx.plusroi.id), {"name": "   "})
    assert err.value.code == "NAME_REQUIRED"


def test_validation_bad_currency(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_client(s, owner(fx.plusroi.id), {"name": "X", "currency": "ZZZ"})
    assert err.value.code == "BAD_CURRENCY"


def test_validation_bad_contact_email(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_client(
            s,
            owner(fx.plusroi.id),
            {"name": "X", "contacts": [{"name": "A", "email": "not-an-email"}]},
        )
    assert err.value.code == "BAD_EMAIL"


def test_validation_bad_credit_limit(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_client(s, owner(fx.plusroi.id), {"name": "X", "credit_limit": "-5"})
    assert err.value.code == "BAD_AMOUNT"


# --------------------------------------------------------------- update


def test_update_client_identity_and_profile(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    res = svc.update_client(
        s,
        owner(fx.plusroi.id),
        client_id,
        {
            "name": "Copernic 2",
            "official_name": "Copernic Global",
            "currency": "USD",
            "archived": True,
        },
    )
    assert res["ok"] and res["changed"]
    assert res["row"]["name"] == "Copernic 2"
    assert res["row"]["official_name"] == "Copernic Global"
    assert res["row"]["currency"] == "USD"
    assert res["row"]["archived"] is True


def test_update_client_cross_tenant_404(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    with pytest.raises(ServiceError) as err:
        svc.update_client(s, owner(fx.other.id), client_id, {"name": "Z"})
    assert err.value.code == "CLIENT_NOT_FOUND"


# --------------------------------------------------------------- contacts


def test_add_contact_role_and_listing(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    added = svc.add_contact(
        s, owner(fx.plusroi.id), client_id, {"name": "Cy Contact", "email": "cy@x.com"}
    )
    assert added["ok"] and added["contact"]["role"] == "cc_recipient"

    person = s.get(Party, pid(added["contact"]["party_id"]))
    assert person.kind == "person" and person.email == "cy@x.com"

    listed = svc.list_contacts(s, owner(fx.plusroi.id))["rows"]
    assert len(listed) == 1
    entry = listed[0]
    assert entry["name"] == "Cy Contact"
    assert entry["clients"][0]["role"] == "cc_recipient"
    assert entry["clients"][0]["client_party_id"] == str(client_id)


def test_add_contact_requires_email(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    with pytest.raises(ServiceError) as err:
        svc.add_contact(s, owner(fx.plusroi.id), client_id, {"name": "No Email"})
    assert err.value.code == "BAD_EMAIL"


def test_add_contact_unknown_client_404(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.add_contact(
            s, owner(fx.plusroi.id), uuid.uuid4(), {"name": "A", "email": "a@b.com"}
        )
    assert err.value.code == "CLIENT_NOT_FOUND"


def test_remove_contact_ends_affiliation(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    added = svc.add_contact(
        s, owner(fx.plusroi.id), client_id, {"name": "Cy", "email": "cy@x.com"}
    )
    aff_id = pid(added["contact"]["affiliation_id"])

    svc.remove_contact(s, owner(fx.plusroi.id), aff_id)

    aff = s.get(PartyAffiliation, aff_id)
    assert aff.ended_on is not None  # ended, not hard-deleted (append-only)
    assert svc.list_contacts(s, owner(fx.plusroi.id))["rows"] == []
    assert svc.get_client(s, owner(fx.plusroi.id), client_id)["client"]["contact_count"] == 0


def test_remove_contact_cross_tenant_404(s, fx):
    client_id = pid(
        svc.create_client(s, owner(fx.plusroi.id), {"name": "Copernic"})["row"]["party_id"]
    )
    aff_id = pid(
        svc.add_contact(
            s, owner(fx.plusroi.id), client_id, {"name": "Cy", "email": "cy@x.com"}
        )["contact"]["affiliation_id"]
    )
    with pytest.raises(ServiceError) as err:
        svc.remove_contact(s, owner(fx.other.id), aff_id)
    assert err.value.code == "CONTACT_NOT_FOUND"
