"""T-TEAM rows on the SQLite graph slice — the R1 team-admin behaviors
mapped onto parties + engagements + compensation_terms (02 §1–2).

Carried IDs are noted; where the legacy behavior was a wart the rebuild
fixes, the R1 replacement is tested instead and the divergence is
recorded in docs/features/team.md + ADR #008C.
"""

from datetime import date, timedelta
from decimal import Decimal

import pytest
from sqlalchemy import select

from app.models import RateOverride
from app.services import team_admin
from app.services.authz.context import Actor
from app.services.errors import ServiceError
from app.services.money.stamping import resolve_rates, resolve_worker, stamp_entry
from tests.team_admin.conftest import actor_for


def _list(g, actor):
    return team_admin.list_workers(g.session, actor)


def _worker(payload, email):
    return next(w for w in payload["workers"] if w["email"] == email)


# ---------------------------------------------------------------- add worker


def test_add_worker_creates_party_engagement_and_terms(g, rian):
    res = team_admin.add_worker(
        g.session,
        rian,
        {"name": "Zeina", "email": "zeina@example.com", "cost_rate": "8", "billout_rate": "30"},
    )
    assert res["ok"] and res["changed"]
    payload = _list(g, actor_for(g, "rian"))
    zeina = _worker(payload, "zeina@example.com")
    assert zeina["active"] is True
    assert zeina["cost_term"]["rate"] == "8.00"
    assert zeina["billout_term"]["rate"] == "30.00"
    # a real worker engagement now resolves for imports
    assert resolve_worker(g.session, g.tenant.id, "zeina@example.com") is not None


def test_add_worker_duplicate_active_email_rejected(g, rian):
    # T-TEAM-01 (reworded to R1 tenant-wide uniqueness — teams dissolved)
    with pytest.raises(ServiceError) as exc:
        team_admin.add_worker(
            g.session, rian, {"name": "Adi Again", "email": "info@adipramono.com"}
        )
    assert exc.value.code == "ALREADY_MEMBER"


def test_add_worker_email_trimmed_and_lowercased(g, rian):
    # T-TEAM-04
    team_admin.add_worker(
        g.session, rian, {"name": "Zeina", "email": "  ZEINA@Example.COM "}
    )
    payload = _list(g, actor_for(g, "rian"))
    zeina = _worker(payload, "zeina@example.com")
    assert zeina["email"] == "zeina@example.com"
    # import matching is case-insensitive end-to-end
    assert resolve_worker(g.session, g.tenant.id, "Zeina@EXAMPLE.com") is not None


def test_add_worker_bad_email_rejected(g, rian):
    with pytest.raises(ServiceError) as exc:
        team_admin.add_worker(g.session, rian, {"name": "X", "email": "not-an-email"})
    assert exc.value.code == "BAD_EMAIL"


def test_add_worker_null_cost_stamps_null(g, rian):
    # T-TEAM-07: blank cost -> no cost term -> a stamped entry gets NULL cost
    res = team_admin.add_worker(
        g.session, rian, {"name": "Nora", "email": "nora@example.com", "billout_rate": "40"}
    )
    import uuid

    eng_id = uuid.UUID(res["engagement_id"])
    party_id = uuid.UUID(res["party_id"])
    rates = resolve_rates(g.session, g.tenant.id, eng_id, party_id, None)
    assert rates.cost_rate is None  # cost-unknown, never coalesced to 0
    assert rates.billout_rate == Decimal("40")
    payload = _list(g, actor_for(g, "rian"))
    assert _worker(payload, "nora@example.com")["cost_term"] is None


def test_gary_zero_cost_is_a_real_stamp(g):
    # T-TEAM-08: Gary cost 0 / billout 25 -> 10h stamps cost 0.00, billout 250.00
    rates = resolve_rates(
        g.session, g.tenant.id, g.engagements.gary.id, g.parties.gary.id, None
    )
    stamp = stamp_entry(36000, rates)
    assert stamp.cost_amount == Decimal("0.00")  # NOT NULL — a zero-cost billable worker
    assert stamp.billout_amount == Decimal("250.00")


# ---------------------------------------------------------------- edit worker


def test_update_without_billout_field_preserves_rate(g, rian):
    # T-TEAM-06: a patch that omits billout_rate must not clobber it
    before = _worker(_list(g, rian), "info@adipramono.com")["billout_term"]["rate"]
    team_admin.update_worker(g.session, rian, g.parties.adi.id, {"name": "Adi P."})
    after = _worker(_list(g, actor_for(g, "rian")), "info@adipramono.com")
    assert after["name"] == "Adi P."
    assert after["billout_term"]["rate"] == before  # untouched


def test_rate_change_is_append_only(g, rian):
    # cost 14 -> 20: a NEW term opens, the old one gets effective_to; the
    # existing entry's stamp is NOT touched here.
    from sqlalchemy import select

    from app.models import CompensationTerm

    entry_before = (g.session.get(type(g.entries["adi"]), g.entries["adi"].id).cost_amount)
    team_admin.update_worker(g.session, rian, g.parties.adi.id, {"cost_rate": "20"})

    cost_terms = g.session.scalars(
        select(CompensationTerm).where(
            CompensationTerm.engagement_id == g.engagements.adi.id,
            CompensationTerm.kind == "cost",
        )
    ).all()
    assert len(cost_terms) == 2  # append, not overwrite
    open_terms = [t for t in cost_terms if t.effective_to is None]
    closed = [t for t in cost_terms if t.effective_to is not None]
    assert len(open_terms) == 1 and open_terms[0].rate_amount == Decimal("20.00")
    assert len(closed) == 1 and closed[0].rate_amount == Decimal("14.00")
    assert closed[0].effective_to == date.today() - timedelta(days=1)
    # existing stamp byte-unchanged (money re-stamp is a separate /entries action)
    entry_after = g.session.get(type(g.entries["adi"]), g.entries["adi"].id).cost_amount
    assert entry_after == entry_before


def test_rate_change_no_op_when_same(g, rian):
    res = team_admin.update_worker(g.session, rian, g.parties.adi.id, {"cost_rate": "14"})
    assert res["rate_changes"] == []  # 14 -> 14 changes nothing


def test_consolidate_as_roundtrip(g, rian):
    # T-TEAM-10 (the label lives on the engagement — import-mapping config)
    team_admin.update_worker(
        g.session, rian, g.parties.adi.id, {"consolidate_as": "Tingang"}
    )
    assert _worker(_list(g, actor_for(g, "rian")), "info@adipramono.com")[
        "consolidate_as"
    ] == "Tingang"
    team_admin.update_worker(g.session, rian, g.parties.adi.id, {"consolidate_as": ""})
    assert _worker(_list(g, actor_for(g, "rian")), "info@adipramono.com")[
        "consolidate_as"
    ] is None


# ---------------------------------------------------------------- deactivate


def test_deactivate_ends_engagement_keeps_history(g, rian):
    # R1 replacement for T-TEAM-09 (hard-delete SET NULL): soft archive by
    # ending the engagement; entries + stamps intact; worker stops resolving;
    # Gary's 0/25 terms remain representable.
    entry = g.entries["gary"]
    cost_before = g.session.get(type(entry), entry.id).cost_amount

    res = team_admin.set_active(g.session, rian, g.parties.gary.id, active=False)
    assert res["changed"]

    payload = _list(g, actor_for(g, "rian"))
    gary_row = _worker(payload, "gary@rian.ca")
    assert gary_row["active"] is False and gary_row["ended_on"] is not None
    # terms survive — 0/25 still there
    assert gary_row["cost_term"]["rate"] == "0.00"
    assert gary_row["billout_term"]["rate"] == "25.00"
    # entries + stamps untouched
    assert g.session.get(type(entry), entry.id).cost_amount == cost_before
    # an ended worker no longer resolves on import
    assert resolve_worker(g.session, g.tenant.id, "gary@rian.ca") is None

    # reactivate restores resolution
    team_admin.set_active(g.session, actor_for(g, "rian"), g.parties.gary.id, active=True)
    assert resolve_worker(g.session, g.tenant.id, "gary@rian.ca") is not None


# ---------------------------------------------------------------- rate overrides


def test_rate_override_precedence(g, rian):
    # T-TEAM-11: worker-specific > project-wide > term; pct modifies the
    # term-resolved default. Delegates to stamping (judgment #25).
    import uuid

    site = g.projects.site.id
    team_admin.create_rate_override(
        g.session,
        rian,
        {
            "project_id": str(site),
            "worker_party_id": str(g.parties.adi.id),
            "override_rate": "20",
            "currency": "CAD",
        },
    )
    # project-wide pct override (worker NULL)
    team_admin.create_rate_override(
        g.session,
        actor_for(g, "rian"),
        {"project_id": str(site), "override_pct": "-20"},
    )

    rates = resolve_rates(
        g.session, g.tenant.id, g.engagements.adi.id, g.parties.adi.id, site
    )
    assert rates.billout_rate == Decimal("20")  # worker-specific absolute wins

    # drop the worker-specific rule -> project-wide pct applies to the 35 default
    overrides = team_admin.list_workers(g.session, actor_for(g, "rian"))["rate_overrides"]
    specific = next(o for o in overrides if o["worker_party_id"] == str(g.parties.adi.id))
    team_admin.delete_rate_override(g.session, actor_for(g, "rian"), uuid.UUID(specific["id"]))
    rates2 = resolve_rates(
        g.session, g.tenant.id, g.engagements.adi.id, g.parties.adi.id, site
    )
    assert rates2.billout_rate == Decimal("28.00")  # 35 * (1 - 0.20)


def test_rate_override_xor_validation(g, rian):
    site = str(g.projects.site.id)
    with pytest.raises(ServiceError) as both:
        team_admin.create_rate_override(
            g.session, rian,
            {"project_id": site, "override_rate": "20", "override_pct": "-10", "currency": "CAD"},
        )
    assert both.value.code == "OVERRIDE_ONE_KIND"
    with pytest.raises(ServiceError) as neither:
        team_admin.create_rate_override(g.session, actor_for(g, "rian"), {"project_id": site})
    assert neither.value.code == "OVERRIDE_ONE_KIND"
    with pytest.raises(ServiceError) as no_ccy:
        team_admin.create_rate_override(
            g.session, actor_for(g, "rian"), {"project_id": site, "override_rate": "20"}
        )
    assert no_ccy.value.code == "CURRENCY_REQUIRED"


def test_rate_override_pct_lower_bound(g, rian):
    """T-ADJ-009: pct < -100% is rejected (would make billout negative);
    -100 (free) is the allowed floor and stores un-clamped."""
    site = str(g.projects.site.id)
    with pytest.raises(ServiceError) as too_low:
        team_admin.create_rate_override(
            g.session, rian, {"project_id": site, "override_pct": "-150"}
        )
    assert too_low.value.code == "PCT_TOO_LOW"
    # the validator rejects BEFORE any write — no stray row from the -150 try
    assert (
        g.session.scalar(
            select(RateOverride).where(RateOverride.project_id == g.projects.site.id)
        )
        is None
    )
    # -100 (free) is the boundary and allowed; stored un-clamped
    floor = team_admin.create_rate_override(
        g.session, actor_for(g, "rian"), {"project_id": site, "override_pct": "-100"}
    )
    assert floor["ok"] is True
    row = g.session.scalar(
        select(RateOverride).where(RateOverride.project_id == g.projects.site.id)
    )
    assert row is not None and row.override_pct == Decimal("-100.0000")


def test_rate_override_duplicate_rejected(g, rian):
    site = str(g.projects.site.id)
    team_admin.create_rate_override(
        g.session, rian, {"project_id": site, "override_pct": "-20"}
    )
    with pytest.raises(ServiceError) as exc:
        team_admin.create_rate_override(
            g.session, actor_for(g, "rian"), {"project_id": site, "override_pct": "-10"}
        )
    assert exc.value.code == "OVERRIDE_EXISTS"


# ---------------------------------------------------------------- permissions


def _owner_without_billout(g) -> Actor:
    """A hypothetical actor that can manage the team but cannot see the
    owner's private billout number (the R2 per-team-owner grade). In R1
    the two capabilities coincide on rian, so we build the split actor
    directly to pin the payload's billout-gating rule (T-TEAM-12)."""
    return Actor(
        tenant_id=g.tenant.id, tenant_name="Bowden Works", tenant_slug="bowden-works",
        real_user_id=g.users["rian"].id, real_username="ops", real_email="ops@x",
        real_is_super_admin=False,
        user_id=g.users["rian"].id, username="ops", email="ops@x",
        is_super_admin=False, person_party_id=None, role="manager", role_reason="test",
        capabilities={
            "can_manage_team": True, "can_transfer": False, "can_see_income": False,
            "can_manage_imports": True, "can_enter_view_as": False,
        },
    )


def test_non_owner_payload_hides_billout(g):
    # T-TEAM-12: can_transfer False -> no billout_term key, no rate-override list
    payload = team_admin.list_workers(g.session, _owner_without_billout(g))
    assert payload["can_see_billout"] is False
    for w in payload["workers"]:
        assert "billout_term" not in w
        assert "cost_term" in w  # cost still visible to a team manager
    assert payload["rate_overrides"] == []


def test_manager_and_worker_cannot_manage_team(g, adi, gary):
    # can_manage_team is owner/super-admin only in R1 (02 §4 effective rule;
    # per-team-owner grants -> R2). Adi (manager) and Gary (worker) are denied.
    for actor in (adi, gary):
        with pytest.raises(ServiceError) as read_exc:
            team_admin.list_workers(g.session, actor)
        assert read_exc.value.code == "NOT_ALLOWED"
        with pytest.raises(ServiceError) as add_exc:
            team_admin.add_worker(g.session, actor, {"name": "X", "email": "x@y.co"})
        assert add_exc.value.code == "NOT_ALLOWED"
    with pytest.raises(ServiceError):
        team_admin.set_active(g.session, adi, g.parties.gary.id, active=False)
