"""Team-admin test harness.

Two fixture families:

  * `g` — a small SQLite slice of the party/engagement/term graph
    (04-architecture: "fixtures are small slices of the same graph, so
    tests speak the business's language"): rian (owner + super admin),
    adi (manager-grade subcontract, cost 14 / billout 35 CAD), gary (the
    zero-cost billable AI, cost 0 / billout 25), a Site Rebuild project
    for rate-override tests, and one stamped entry each for adi/gary so
    entry counts and stamp-preservation are checkable. Portable
    constructs only — team_admin uses nothing Postgres-specific.

  * the sidecar-Postgres scratch (`m7_engine` / `s`) — its OWN database
    (`with_m7_team_test`, independent of the M5 bulk scratch), migrated
    with the real alembic chain, loaded with the fixture legacy dataset
    and transformed once, so the append-only GATE runs over a REAL
    migrated worker (Adi) in exactly the shape production has.
"""

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

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

from app.models import (
    Base,
    CompensationTerm,
    Engagement,
    ImportBatch,
    Party,
    Project,
    Tenant,
    TimeEntry,
    User,
)
from app.services.authz.context import resolve_actor

TENANT_SLUG = "bowden-works"

ROOT = Path(__file__).resolve().parents[2]
FIXTURE_SQL = ROOT / "tests" / "migrate" / "fixtures" / "legacy_fixture.sql"
TEST_DB = "with_m7_team_test"

# fixture time-entry ids carry through the migration (05 step 2.6 id-carry)
TE = {n: uuid.UUID(f"0a000000-0000-0000-0000-00000000000{n}") for n in range(1, 8)}


def fake_request(username: str, cookies: dict | None = None):
    return SimpleNamespace(
        state=SimpleNamespace(identity={"username": username, "email": ""}),
        cookies=cookies or {},
    )


# ---------------------------------------------------------------- SQLite slice


def _party(s, kind, name, slug, email=None):
    party = Party(kind=kind, name=name, slug=slug, email=email)
    s.add(party)
    s.flush()
    return party


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

    bw = _party(s, "org", "Bowden Works", TENANT_SLUG)
    plusroi = _party(s, "org", "PlusROI", "plusroi")
    brentwood = _party(s, "org", "Brentwood", "brentwood")
    tenant = Tenant(party_id=bw.id, settings={"default_currency": "CAD"})
    s.add(tenant)
    s.flush()

    p_rian = _party(s, "person", "Rian Bowden", "rian-bowden", "rian@rian.ca")
    p_adi = _party(s, "person", "Adi Pramono", "adi-pramono", "info@adipramono.com")
    p_gary = _party(s, "person", "Gary", "gary", "gary@rian.ca")

    users = {
        "rian": User(
            idauth_username="rian", email="rian@rian.ca",
            person_party_id=p_rian.id, is_super_admin=True,
        ),
        "adi": User(
            idauth_username="adi", email="info@adipramono.com", person_party_id=p_adi.id
        ),
        "gary": User(
            idauth_username="gary", email="gary@rian.ca", person_party_id=p_gary.id
        ),
    }
    s.add_all(users.values())
    s.flush()

    def worker_engagement(party, *, cmi=False, cost=None, billout=None, ended=None):
        eng = Engagement(
            tenant_id=tenant.id, type="subcontract",
            party_a_id=bw.id, party_b_id=party.id,
            role_a="prime", role_b="subcontractor", can_manage_imports=cmi,
            ended_on=ended,
        )
        s.add(eng)
        s.flush()
        for kind, rate in (("cost", cost), ("billout", billout)):
            if rate is not None:
                s.add(
                    CompensationTerm(
                        engagement_id=eng.id, kind=kind, model="hourly",
                        rate_amount=rate, currency="CAD",
                        effective_from=date(2026, 1, 1),
                    )
                )
        s.flush()
        return eng

    s.add(
        Engagement(
            tenant_id=tenant.id, type="official_partnership",
            party_a_id=bw.id, party_b_id=p_rian.id, role_a="owner", role_b="owner",
        )
    )
    eng_rian = worker_engagement(p_rian, cost=50, billout=100)
    eng_adi = worker_engagement(p_adi, cmi=True, cost=14, billout=35)
    eng_gary = worker_engagement(p_gary, cost=0, billout=25)

    for op, cl in ((plusroi, brentwood),):
        s.add(
            Engagement(
                tenant_id=tenant.id, type="direct_client",
                party_a_id=op.id, party_b_id=cl.id, role_a="operator", role_b="client",
            )
        )
    pr_site = Project(
        tenant_id=tenant.id, operator_party_id=plusroi.id,
        client_party_id=brentwood.id, name="Site Rebuild", status="active",
    )
    s.add(pr_site)
    s.flush()

    batch = ImportBatch(
        tenant_id=tenant.id, imported_by=users["rian"].id, source="clockify", name="b"
    )
    s.add(batch)
    s.flush()

    def entry(party, eng, **kw):
        defaults = dict(
            tenant_id=tenant.id, source="clockify", import_id=batch.id,
            start_at=datetime(2026, 6, 1, 9, 0, 0),
            end_at=datetime(2026, 6, 1, 10, 0, 0),
            duration_seconds=3600, billable=True,
            worker_party_id=party.id, via_engagement_id=eng.id, project_id=pr_site.id,
        )
        defaults.update(kw)
        e = TimeEntry(**defaults)
        s.add(e)
        s.flush()
        return e

    entries = {
        "adi": entry(p_adi, eng_adi, cost_amount=14, cost_currency="CAD",
                     billout_amount=35, billout_currency="CAD"),
        "gary": entry(p_gary, eng_gary, cost_amount=0, cost_currency="CAD",
                      billout_amount=25, billout_currency="CAD"),
    }
    s.commit()

    yield SimpleNamespace(
        session=s, engine=engine, tenant=tenant, users=users,
        parties=SimpleNamespace(
            bw=bw, plusroi=plusroi, brentwood=brentwood,
            rian=p_rian, adi=p_adi, gary=p_gary,
        ),
        engagements=SimpleNamespace(rian=eng_rian, adi=eng_adi, gary=eng_gary),
        projects=SimpleNamespace(site=pr_site),
        entries=entries,
    )
    s.close()
    engine.dispose()


def actor_for(g, username: str):
    return resolve_actor(fake_request(username), g.session)


@pytest.fixture()
def rian(g):
    return actor_for(g, "rian")


@pytest.fixture()
def adi(g):
    return actor_for(g, "adi")


@pytest.fixture()
def gary(g):
    return actor_for(g, "gary")


# ---------------------------------------------------------------- Postgres scratch


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(
            "team-admin gate needs the sidecar Postgres — set WITH_DB_URL_HOST"
            " in .env (127.0.0.1:54317)"
        )
    return url


@pytest.fixture(scope="session")
def m7_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:
        pytest.fail(f"cannot reach the sidecar Postgres for team-admin 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


@pytest.fixture(scope="session")
def m7_engine(m7_url):
    engine = create_engine(m7_url)
    with engine.begin() as conn:
        conn.exec_driver_sql(FIXTURE_SQL.read_text(encoding="utf-8"))
    from app.services.migrate import run_all

    report = run_all(engine=engine)
    assert report.ok, report.render()
    yield engine
    engine.dispose()


@pytest.fixture()
def s(m7_engine):
    """SAVEPOINT-isolated session: service-level commit()s stay inside the
    outer transaction, which rolls back after the test — so the gate can
    commit real term changes and read them back without polluting the DB."""
    conn = m7_engine.connect()
    trans = conn.begin()
    session = Session(bind=conn, join_transaction_mode="create_savepoint")
    yield session
    session.close()
    trans.rollback()
    conn.close()


def actor_named(session: Session, username: str):
    return resolve_actor(fake_request(username), session)


def real_worker_engagement(session: Session, email: str) -> tuple[Party, Engagement]:
    """The migrated person party + its active worker engagement for a real
    fixture email (e.g. Adi). Proves the gate on production-shaped data."""
    party = session.scalars(
        select(Party).where(Party.kind == "person", Party.email == email.lower())
    ).one()
    tenant_id = session.scalar(
        select(Tenant.id).join(Party, Party.id == Tenant.party_id).where(
            Party.slug == TENANT_SLUG
        )
    )
    eng = session.scalars(
        select(Engagement).where(
            Engagement.tenant_id == tenant_id,
            Engagement.party_b_id == party.id,
            Engagement.type.in_(("subcontract", "freelance")),
            Engagement.ended_on.is_(None),
        )
    ).one()
    return party, eng
