"""Delete (T-ENT-35, undo token), re-resolve (T-ENT-32/33/34 rebuilt:
tenant-wide roster, FULL stamp clear), and the single-row undo
mechanics (replay-from-audit-row, double-undo guard, permissions)."""

from decimal import Decimal

import pytest
from sqlalchemy import select

from app.models import AuditLog, TimeEntry
from app.services.audit import undo_entries_action
from app.services.entries_write import delete_entry, reresolve_entry, update_entry
from app.services.errors import ServiceError


def err(fn, *args) -> ServiceError:
    with pytest.raises(ServiceError) as exc_info:
        fn(*args)
    return exc_info.value


# ------------------------------------------------------------- delete


def test_delete_removes_row_and_returns_undo_token(g, rian):
    e = g.entries["adi"]
    entry_id = e.id
    undo_id = delete_entry(g.session, rian, entry_id)
    assert g.session.get(TimeEntry, entry_id) is None
    audit = g.session.get(AuditLog, undo_id)
    assert audit.action == "entries.delete"
    assert audit.before["rows"][0]["id"] == str(entry_id)


def test_t_ent_35_locked_delete_manager_blocked_owner_ok(g, rian, adi):
    e = g.entries["locked"]
    error = err(delete_entry, g.session, adi, e.id)
    assert error.summary == (
        "This entry is attached to an invoice and locked. Only an owner can delete it."
    )
    delete_entry(g.session, rian, e.id)  # owner surgery allowed (B63)
    assert g.session.get(TimeEntry, e.id) is None


def test_delete_undo_reinserts_verbatim(g, rian):
    e = g.entries["adi"]
    entry_id = e.id
    from app.services.audit import snapshot_entry

    before = snapshot_entry(e)
    undo_id = delete_entry(g.session, rian, entry_id)
    result = undo_entries_action(g.session, rian, undo_id)
    assert result["reinserted"] == 1
    restored = g.session.get(TimeEntry, entry_id)
    assert snapshot_entry(restored) == before  # byte-identical restore


# ------------------------------------------------------------ re-resolve


def test_t_ent_32_unknown_email_errors(g, rian):
    e = g.entries["unresolved"]
    error = err(reresolve_entry, g.session, rian, e.id)
    assert error.summary == (
        "No team member with email unknown@example.com. Add them on the Team page first."
    )


def test_t_ent_33_empty_email_clears_everything_including_billout(g, rian):
    e = g.entries["adi"]
    e.source_user_email = None
    g.session.commit()
    _, info = reresolve_entry(g.session, rian, e.id)
    assert info == "Cleared conversion — no source email."
    assert e.worker_party_id is None and e.via_engagement_id is None
    assert e.cost_amount is None and e.cost_currency is None
    # the legacy stale-billout wart (B54) does not port:
    assert e.billout_amount is None and e.billout_currency is None


def test_reresolve_hit_restamps_from_current_terms(g, rian):
    from app.services.audit import snapshot_entry

    e = g.entries["adi"]
    e.worker_party_id = None
    e.via_engagement_id = None
    e.cost_amount = None
    e.cost_currency = None
    g.session.commit()
    before = snapshot_entry(e)
    _, info = reresolve_entry(g.session, rian, e.id)
    assert info == "Re-resolved."
    assert e.worker_party_id == g.parties.adi.id
    assert e.via_engagement_id == g.engagements.adi.id
    assert Decimal(str(e.cost_amount)) == Decimal("14.00")
    assert Decimal(str(e.billout_amount)) == Decimal("20.00")  # worker override
    # re-resolve is undoable too (replay-from-audit-row)
    result = undo_entries_action(g.session, rian, undo_id_of_last(g, "entries.reresolve"))
    assert result["restored"] == 1
    assert snapshot_entry(e) == before


def test_reresolve_locked_manager_blocked(g, adi):
    e = g.entries["locked"]
    error = err(reresolve_entry, g.session, adi, e.id)
    assert error.summary == (
        "This entry is attached to an invoice and locked. Only an owner can re-resolve it."
    )


# ------------------------------------------------------------- undo


def undo_id_of_last(g, action: str):
    return g.session.scalars(
        select(AuditLog).where(AuditLog.action == action).order_by(AuditLog.created_at)
    ).all()[-1].id


def test_update_undo_restores_exactly(g, rian):
    from app.services.audit import snapshot_entry

    e = g.entries["adi"]
    before = snapshot_entry(e)
    update_entry(g.session, rian, e.id, {"worker_party_id": str(g.parties.gary.id)})
    assert e.worker_party_id == g.parties.gary.id
    result = undo_entries_action(g.session, rian, undo_id_of_last(g, "entries.update"))
    assert result["restored"] == 1
    assert snapshot_entry(e) == before


def test_undo_twice_refused(g, rian):
    e = g.entries["adi"]
    update_entry(g.session, rian, e.id, {"description": "x"})
    undo_id = undo_id_of_last(g, "entries.update")
    undo_entries_action(g.session, rian, undo_id)
    error = err(undo_entries_action, g.session, rian, undo_id)
    assert error.code == "ALREADY_UNDONE"


def test_undo_skips_rows_locked_since(g, rian):
    """A row that got attached to an invoice AFTER the edit must never
    be silently rewritten by the undo (exact-count reporting)."""
    e = g.entries["adi"]
    update_entry(g.session, rian, e.id, {"description": "edited"})
    undo_id = undo_id_of_last(g, "entries.update")
    e.invoice_line_id = g.line.id  # locked since the edit
    g.session.commit()
    result = undo_entries_action(g.session, rian, undo_id)
    assert result["skipped_locked"] == 1
    assert result["restored"] == 0
    assert e.description == "edited"  # untouched


def test_undo_requires_author_or_owner(g, rian, adi):
    e = g.entries["adi"]
    update_entry(g.session, adi, e.id, {"description": "adi edit"})
    undo_id = undo_id_of_last(g, "entries.update")
    # gary (worker) can't undo at all; rian (owner) may undo adi's action
    from tests.entries_write.conftest import actor_for

    error = err(undo_entries_action, g.session, actor_for(g, "gary"), undo_id)
    assert error.status == 403
    result = undo_entries_action(g.session, rian, undo_id)
    assert result["restored"] == 1


def test_undo_view_as_records_both_identities(g):
    """Mutations under view-as audit the EFFECTIVE actor with
    impersonated_by = the real super admin (guard-rail parity)."""
    from app.services.authz.context import resolve_actor
    from app.services.authz.view_as import VIEW_AS_COOKIE
    from tests.authz.conftest import fake_request

    viewing = resolve_actor(fake_request("rian", {VIEW_AS_COOKIE: "adi"}), g.session)
    assert viewing.is_viewing_as and viewing.role == "manager"
    e = g.entries["adi"]
    update_entry(g.session, viewing, e.id, {"description": "via view-as"})
    audit = g.session.scalars(
        select(AuditLog).where(AuditLog.action == "entries.update")
    ).all()[-1]
    assert audit.actor_user_id == g.users["adi"].id
    assert audit.impersonated_by == g.users["rian"].id


def test_view_as_worker_cannot_mutate(g):
    """Guard rails: a super admin viewing-as a worker inherits the
    worker's (lack of) capabilities — view-as is true impersonation."""
    from app.services.authz.context import resolve_actor
    from app.services.authz.view_as import VIEW_AS_COOKIE
    from tests.authz.conftest import fake_request

    viewing = resolve_actor(fake_request("rian", {VIEW_AS_COOKIE: "gary"}), g.session)
    assert viewing.is_viewing_as and viewing.role == "worker"
    e = g.entries["gary"]  # in rian's batch — out of gary's scope anyway
    error = err(update_entry, g.session, viewing, e.id, {"description": "x"})
    assert error.status in (403, 404)
