"""Multi-tenant isolation harness (ADR #015).

A dedicated scratch DB on the sidecar Postgres, migrated to ``head`` and
populated with TWO fully-independent tenants built with ORM fixtures —
Bowden Works (tenant A) and PlusROI (tenant B). Deliberately does NOT run
the legacy transforms (``run_all``): the graph is hand-built so these
tests are hermetic and independent of the migration pipeline (which the
parallel schema agent may have red at any moment).

The critical property under test: a user whose ONLY membership is tenant
B can never see a single tenant-A row through any endpoint, and a
tenant-A object id is a 404 (existence hidden), not a 403.

Users:
  rian   super admin, official_partnership on A   -> sees ALL tenants
  alice  official_partnership on A (owner)         -> A only
  bob    official_partnership on B (owner)         -> B only  (partner analog)
  carol  subcontract on B (worker), imports NONE   -> B only, sees 0 entries
  nobody no engagement anywhere                     -> NO_TENANT
alice also carries an ENDED official_partnership on B (proves an ended
engagement grants no membership).
"""

from __future__ import annotations

import os
from datetime import date, datetime
from pathlib import Path
from types import SimpleNamespace

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session

from app.main import create_app
from app.models import (
    CcExpenseBatch,
    CcExpenseLine,
    Comment,
    Engagement,
    ImportBatch,
    Invoice,
    Project,
    Tenant,
    TimeEntry,
    User,
)
from app.services.auth import session as auth_session
from app.services.entities.parties import create_org_party, create_person_party

ROOT = Path(__file__).resolve().parents[2]
TEST_DB = "with_tenancy_test"


def _sidecar_url() -> str:
    from app.config import settings

    url = settings.with_db_url_host or settings.with_db_url
    if not url:
        pytest.fail(
            "tenancy tests need the sidecar Postgres — set WITH_DB_URL_HOST"
            " in .env (127.0.0.1:54317)"
        )
    return url


@pytest.fixture(scope="session")
def tenancy_url() -> str:
    base = _sidecar_url()
    admin = create_engine(
        base, isolation_level="AUTOCOMMIT", connect_args={"connect_timeout": 3}
    )
    try:
        with admin.connect() as conn:
            conn.execute(text(f"DROP DATABASE IF EXISTS {TEST_DB} WITH (FORCE)"))
            conn.execute(text(f"CREATE DATABASE {TEST_DB}"))
    except Exception as exc:  # pragma: no cover - env guard
        pytest.fail(f"cannot reach the sidecar Postgres for tenancy tests: {exc}")
    finally:
        admin.dispose()

    url = base.rsplit("/", 1)[0] + f"/{TEST_DB}"

    from alembic.config import Config

    from alembic import command

    cfg = Config(str(ROOT / "alembic.ini"))
    cfg.set_main_option("script_location", str(ROOT / "alembic"))
    previous = os.environ.get("WITH_DB_URL")
    os.environ["WITH_DB_URL"] = url
    try:
        command.upgrade(cfg, "head")
    finally:
        os.environ["WITH_DB_URL"] = previous if previous is not None else ""
    return url


def _naive(day: int) -> datetime:
    return datetime(2026, 6, day, 9, 0, 0)


def _populate_tenant(s: Session, *, slug: str, name: str, owner_person, tag: str):
    """Build one tenant's worth of every tenant-scoped object type, so
    isolation can be proven across every endpoint. Returns the ids that
    the isolation tests probe (list contents + direct-GET-by-id)."""
    tenant_party = create_org_party(s, name=name, slug=slug)
    tenant = Tenant(party_id=tenant_party.id, settings={})
    s.add(tenant)
    s.flush()

    # owner engagement (official_partnership -> role owner)
    s.add(
        Engagement(
            tenant_id=tenant.id,
            type="official_partnership",
            party_a_id=tenant_party.id,
            party_b_id=owner_person.id,
            role_a="owner",
            role_b="owner",
        )
    )

    client = create_org_party(s, name=f"{tag} Client", slug=f"{slug}-client")
    s.add(
        Engagement(
            tenant_id=tenant.id,
            type="direct_client",
            party_a_id=tenant_party.id,
            party_b_id=client.id,
            role_a="operator",
            role_b="client",
        )
    )
    s.flush()

    project = Project(
        tenant_id=tenant.id,
        operator_party_id=tenant_party.id,
        client_party_id=client.id,
        name=f"{tag} Website",
        status="active",
    )
    s.add(project)
    s.flush()

    batch = ImportBatch(
        tenant_id=tenant.id, source="clockify", name=f"{tag} batch", row_count=2
    )
    s.add(batch)
    s.flush()

    entries = []
    for i in range(2):
        entry = TimeEntry(
            tenant_id=tenant.id,
            project_id=project.id,
            worker_party_id=owner_person.id,
            source="clockify",
            import_id=batch.id,
            source_user_name=f"{tag} Worker",
            source_user_email=f"worker@{slug}.test",
            raw_operator=name,
            raw_client=f"{tag} Client",
            raw_project=f"{tag} Website",
            description=f"{tag} work {i}",
            billable=True,
            start_at=_naive(2 + i),
            end_at=_naive(2 + i),
            duration_seconds=3600,
            cost_amount=18,
            cost_currency="CAD",
            billout_amount=25,
            billout_currency="CAD",
        )
        s.add(entry)
        entries.append(entry)
    # a blocked (unassigned) entry — no project, still tenant-scoped
    s.add(
        TimeEntry(
            tenant_id=tenant.id,
            source="clockify",
            import_id=batch.id,
            raw_operator=name,
            description=f"{tag} blocked",
            start_at=_naive(4),
            duration_seconds=1800,
        )
    )
    s.flush()

    invoice = Invoice(
        tenant_id=tenant.id, name=f"{tag} INV-001", status="open", currency="CAD",
        from_party_id=tenant_party.id, to_party_id=client.id,
    )
    s.add(invoice)

    comment = Comment(tenant_id=tenant.id, body=f"{tag} board note")
    s.add(comment)

    cc_batch = CcExpenseBatch(tenant_id=tenant.id, name=f"{tag} Visa", currency="CAD")
    s.add(cc_batch)
    s.flush()
    s.add(
        CcExpenseLine(
            batch_id=cc_batch.id,
            line_index=0,
            raw_line=f"{tag} GOOGLE 12.00",
            expense_date=date(2026, 6, 3),
            description="GOOGLE",
            amount=12,
        )
    )
    s.flush()

    return SimpleNamespace(
        tenant=tenant,
        tenant_party=tenant_party,
        slug=slug,
        name=name,
        project_id=str(project.id),
        batch_id=str(batch.id),
        invoice_id=str(invoice.id),
        cc_batch_id=str(cc_batch.id),
        entry_ids={str(e.id) for e in entries},
    )


@pytest.fixture(scope="session")
def tenancy_engine(tenancy_url):
    engine = create_engine(tenancy_url)
    with Session(engine) as s:
        p_rian = create_person_party(s, name="Rian Bowden", slug="t-rian", email="rian@rian.ca")
        p_alice = create_person_party(s, name="Alice A", slug="t-alice", email="alice@a.test")
        p_bob = create_person_party(s, name="Bob B", slug="t-bob", email="bob@b.test")
        p_carol = create_person_party(s, name="Carol B", slug="t-carol", email="carol@b.test")
        p_nobody = create_person_party(s, name="Nemo", slug="t-nemo", email="nemo@x.test")
        s.flush()

        s.add_all(
            [
                User(idauth_username="rian", email="rian@rian.ca",
                     person_party_id=p_rian.id, is_super_admin=True),
                User(idauth_username="alice", email="alice@a.test",
                     person_party_id=p_alice.id),
                User(idauth_username="bob", email="bob@b.test",
                     person_party_id=p_bob.id),
                User(idauth_username="carol", email="carol@b.test",
                     person_party_id=p_carol.id),
                User(idauth_username="nobody", email="nemo@x.test",
                     person_party_id=p_nobody.id),
            ]
        )
        s.flush()

        a = _populate_tenant(s, slug="bowden-works", name="Bowden Works",
                             owner_person=p_alice, tag="BW")
        b = _populate_tenant(s, slug="plusroi", name="PlusROI",
                             owner_person=p_bob, tag="PR")

        # rian: super admin + an owner engagement on A (sees everything anyway)
        s.add(
            Engagement(
                tenant_id=a.tenant.id, type="official_partnership",
                party_a_id=a.tenant_party.id, party_b_id=p_rian.id,
                role_a="owner", role_b="owner",
            )
        )
        # carol: a worker on B who has imported nothing (sees zero entries)
        s.add(
            Engagement(
                tenant_id=b.tenant.id, type="subcontract",
                party_a_id=b.tenant_party.id, party_b_id=p_carol.id,
                role_a="prime", role_b="subcontractor",
            )
        )
        # alice: an ENDED owner engagement on B — must grant NO B membership
        s.add(
            Engagement(
                tenant_id=b.tenant.id, type="official_partnership",
                party_a_id=b.tenant_party.id, party_b_id=p_alice.id,
                role_a="owner", role_b="owner",
                started_on=date(2025, 1, 1), ended_on=date(2025, 12, 31),
            )
        )
        s.commit()

        people = SimpleNamespace(
            rian=p_rian, alice=p_alice, bob=p_bob, carol=p_carol, nobody=p_nobody
        )
        engine._graph = SimpleNamespace(A=a, B=b, people=people)  # type: ignore[attr-defined]

    yield engine
    engine.dispose()


@pytest.fixture()
def graph(tenancy_engine):
    return tenancy_engine._graph  # type: ignore[attr-defined]


@pytest.fixture()
def client_factory(tenancy_engine, monkeypatch):
    from app import db as app_db

    monkeypatch.setattr(app_db, "get_engine", lambda: tenancy_engine)
    clients: list[TestClient] = []

    def make(username: str, *, is_super_admin: bool = False,
             tenant_cookie: str | None = None) -> TestClient:
        client = TestClient(create_app(), base_url="https://testserver")
        client.__enter__()
        clients.append(client)
        token = auth_session.mint_session(
            username=username, email=f"{username}@x.test", is_super_admin=is_super_admin
        )
        client.cookies.set(auth_session.SESSION_COOKIE, token)
        if tenant_cookie is not None:
            client.cookies.set("with_tenant", tenant_cookie)
        return client

    yield make
    for client in clients:
        client.__exit__(None, None, None)


@pytest.fixture()
def session(tenancy_engine):
    with Session(tenancy_engine) as s:
        yield s
