"""Vendors write service (services/vendors.py; phase-3 slice 1).

SQLite fixtures per the house style: an app-owned tenant (writable), a
sheet-owned tenant (write-block), and owner/worker actors.
"""

import uuid
from datetime import date
from decimal import Decimal
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, Expense, Party, Tenant, TenantVendor
from app.services import vendors 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


@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


@pytest.fixture()
def fx(s):
    plusroi = _tenant(s, "PlusROI", "plusroi", {"books_owner": "app"})
    sheet = _tenant(s, "SheetCo", "sheetco", {"books_owner": "sheet"})
    s.flush()
    return SimpleNamespace(plusroi=plusroi, sheet=sheet)


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 _party_count(s, kind):
    return s.scalar(select(func.count()).select_from(Party).where(Party.kind == kind))


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


def test_create_org_vendor(s, fx):
    res = svc.create_vendor(s, owner(fx.plusroi.id), {"name": "Google", "kind": "org"})
    assert res["ok"] and res["reused_party"] is False
    assert res["row"]["kind"] == "org" and res["row"]["name"] == "Google"
    party = s.get(Party, uuid.UUID(res["row"]["party_id"]))
    assert party.kind == "org"


def test_create_person_vendor(s, fx):
    res = svc.create_vendor(
        s, owner(fx.plusroi.id), {"name": "Jane Freelancer", "kind": "person"}
    )
    assert res["row"]["kind"] == "person"
    party = s.get(Party, uuid.UUID(res["row"]["party_id"]))
    assert party.kind == "person"


def test_reuse_existing_by_name_no_dup(s, fx):
    existing = create_org_party(s, name="Google", slug=_allocate_slug(s, "Google"))
    s.commit()
    before = _party_count(s, "org")

    res = svc.create_vendor(s, owner(fx.plusroi.id), {"name": "google", "kind": "org"})
    assert res["reused_party"] is True
    assert res["row"]["party_id"] == str(existing.id)
    assert _party_count(s, "org") == before


def test_org_and_person_same_name_stay_distinct(s, fx):
    org = svc.create_vendor(s, owner(fx.plusroi.id), {"name": "Sam", "kind": "org"})
    person = svc.create_vendor(s, owner(fx.plusroi.id), {"name": "Sam", "kind": "person"})
    assert org["row"]["party_id"] != person["row"]["party_id"]
    assert org["reused_party"] is False and person["reused_party"] is False


def test_list_vendors_from_expenses(s, fx):
    vendor_id = svc.create_vendor(
        s, owner(fx.plusroi.id), {"name": "Google", "kind": "org"}
    )["row"]["party_id"]

    s.add(
        Expense(
            tenant_id=fx.plusroi.id,
            vendor_party_id=uuid.UUID(vendor_id),
            amount=Decimal("10.00"),
            currency="CAD",
            expense_date=date(2026, 4, 1),
            source="manual",
        )
    )
    s.flush()
    rows = svc.list_vendors(s, owner(fx.plusroi.id))["rows"]
    assert len(rows) == 1  # a marker + an expense collapse to ONE row
    assert rows[0]["party_id"] == vendor_id and rows[0]["expense_count"] == 1


# ------------------------------------------- listability (slice 2a fix)


def test_new_vendor_listable_immediately(s, fx):
    # THE slice-2a fix: a vendor with NO expenses yet is now listable
    # (a tenant_vendors marker is written on create).
    res = svc.create_vendor(s, owner(fx.plusroi.id), {"name": "Airtable", "kind": "org"})
    party_id = res["row"]["party_id"]
    assert res["row"]["listed_via"] == "marker"

    marker = s.get(TenantVendor, (fx.plusroi.id, uuid.UUID(party_id)))
    assert marker is not None

    rows = svc.list_vendors(s, owner(fx.plusroi.id))["rows"]
    assert [r["party_id"] for r in rows] == [party_id]
    assert rows[0]["expense_count"] == 0  # listable with zero expenses


def test_vendor_defaults_stored_and_returned(s, fx):
    res = svc.create_vendor(
        s,
        owner(fx.plusroi.id),
        {"name": "Google Ads", "kind": "org", "default_category": "PPC"},
    )
    assert res["row"]["default_category"] == "PPC"
    listed = svc.list_vendors(s, owner(fx.plusroi.id))["rows"]
    assert listed[0]["default_category"] == "PPC"


def test_marker_is_tenant_scoped(s, fx):
    # a vendor marker created for PlusROI does NOT list for the sheet tenant
    # (which is app-owned? no — sheet is sheet-owned; use a fresh app tenant)
    svc.create_vendor(s, owner(fx.plusroi.id), {"name": "Notion", "kind": "org"})
    # PlusROI sees it
    assert any(
        r["name"] == "Notion" for r in svc.list_vendors(s, owner(fx.plusroi.id))["rows"]
    )
    # a second app-owned tenant does not
    other = _tenant(s, "Other", "other", {"books_owner": "app"})
    assert svc.list_vendors(s, owner(other.id))["rows"] == []


def test_recreate_vendor_refreshes_defaults_no_dup(s, fx):
    o = owner(fx.plusroi.id)
    first = svc.create_vendor(s, o, {"name": "Stripe", "kind": "org"})
    party_id = first["row"]["party_id"]
    # a re-create reuses the party AND the marker, and can set defaults
    again = svc.create_vendor(
        s, o, {"name": "stripe", "kind": "org", "default_category": "Fees"}
    )
    assert again["row"]["party_id"] == party_id
    assert again["row"]["default_category"] == "Fees"
    # exactly ONE marker + ONE listed row
    assert (
        s.scalar(
            select(func.count())
            .select_from(TenantVendor)
            .where(TenantVendor.tenant_id == fx.plusroi.id)
        )
        == 1
    )
    assert len(svc.list_vendors(s, o)["rows"]) == 1


# --------------------------------------------------------------- authz


def test_owner_gate_403(s, fx):
    for call in (
        lambda: svc.list_vendors(s, worker(fx.plusroi.id)),
        lambda: svc.create_vendor(s, worker(fx.plusroi.id), {"name": "X"}),
    ):
        with pytest.raises(ServiceError) as err:
            call()
        assert err.value.code == "NOT_ALLOWED" and err.value.status == 403


def test_sheet_owned_409(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_vendor(s, owner(fx.sheet.id), {"name": "X"})
    assert err.value.code == "SHEET_OWNED_READ_ONLY" and err.value.status == 409


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


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


def test_validation_bad_kind(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_vendor(s, owner(fx.plusroi.id), {"name": "X", "kind": "robot"})
    assert err.value.code == "BAD_KIND"
