"""The bulk-safety suite (judgment #11 / 06-test-plan §7.2) over the
MIGRATED fixture graph: same-filter-scope (T-ENT-16), the safety stop
(T-ENT-17), expected-count on every mutation + undo (T-ENT-44), lock
guards (T-ENT-25, T-ADJ-017), quick-create (T-ENT-24), verified
counts, and view-as/ids containment."""

import uuid

import pytest
from sqlalchemy import select

from app.models import Party, Project, TimeEntry
from app.services.audit import snapshot_entry, undo_entries_action
from app.services.bulk import (
    BulkSelection,
    bulk_delete,
    bulk_edit_fields,
    bulk_recalculate,
    collect_scope,
)
from app.services.errors import ServiceError
from app.services.reporting.entries import entry_totals, list_entries
from app.services.reporting.filters import EntryFilters
from tests.bulk.conftest import TE, entry


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


def gary_party_id(s) -> uuid.UUID:
    return s.scalars(
        select(Party.id).where(Party.kind == "person", Party.email == "gary@rian.ca")
    ).one()


def all_snapshots(s) -> dict[str, dict]:
    rows = s.scalars(select(TimeEntry).order_by(TimeEntry.id)).all()
    return {str(r.id): snapshot_entry(r) for r in rows}


# --------------------------------------------------- same-filter-scope


PENDING_PLUSROI = EntryFilters(status="pending", operator="PlusROI")


def test_t_ent_16_scope_equals_display_exactly(s, rian):
    """The regression shape of the 3,274-row incident: a filter with
    status + operator dimensions. The mutation scope must be EXACTLY
    the displayed row set — both come from one code path."""
    page = list_entries(s, rian, PENDING_PLUSROI)
    display_ids = {row["id"] for row in page.rows}
    assert page.total_count == len(display_ids)  # single page here

    rows = collect_scope(
        s, rian, BulkSelection(expected_count=page.total_count, filters=PENDING_PLUSROI)
    )
    assert {str(r.id) for r in rows} == display_ids
    assert display_ids == {str(TE[2]), str(TE[7])}  # pinned: the fixture's shape


@pytest.mark.parametrize(
    "filters",
    [
        EntryFilters(),
        EntryFilters(status="pending"),
        EntryFilters(status="blocked"),
        EntryFilters(status="applied"),
        EntryFilters(status="all", operator="PlusROI"),
        EntryFilters(status="pending", q="work"),
        EntryFilters(status="all", client="brent"),
        EntryFilters(status="all", user="rian"),
        EntryFilters(status="all", source_email="adipramono"),
        EntryFilters(status="pending", project="Site"),
    ],
)
def test_scope_property_on_migrated_fixture(s, rian, filters):
    """Property: for ANY filter combo, |scope| == the display totals
    row_count, and the id sets agree with the display list."""
    n = entry_totals(s, rian, filters)["row_count"]
    rows = collect_scope(s, rian, BulkSelection(expected_count=n, filters=filters))
    assert len(rows) == n
    display_ids = {row["id"] for row in list_entries(s, rian, filters).rows}
    assert {str(r.id) for r in rows} == display_ids


def test_scope_respects_authz_for_manager(s, adi):
    """Manager scope: only his own batch's rows — the mutation scope
    inherits scope() exactly like the display."""
    n = entry_totals(s, adi, EntryFilters())["row_count"]
    assert n == 4  # te-1..te-4 (his clockify batch)
    rows = collect_scope(s, adi, BulkSelection(expected_count=n, filters=EntryFilters()))
    assert {r.id for r in rows} == {TE[1], TE[2], TE[3], TE[4]}


# --------------------------------------------------------- safety stop


def test_t_ent_17_expected_count_mismatch_aborts(s, rian):
    """415-vs-343 in miniature: the filter matches 2 but the user
    confirmed 1 -> exact message, ZERO rows changed."""
    before = all_snapshots(s)
    error = err(
        bulk_edit_fields,
        s,
        rian,
        BulkSelection(expected_count=1, filters=PENDING_PLUSROI),
        worker_party_id=gary_party_id(s),
    )
    assert error.status == 409 and error.code == "SAFETY_STOP"
    assert error.summary == (
        "Safety stop: the filter currently matches 2 entries, but you confirmed 1."
        " Nothing was changed. Refresh /entries to see the current state, then retry."
    )
    s.rollback()
    assert all_snapshots(s) == before


def test_safety_stop_on_explicit_ids_too(s, rian):
    """T-ENT-44: EVERY mutation carries the precondition — a vanished
    row in an explicit selection aborts as well."""
    ghost = uuid.uuid4()
    error = err(
        bulk_recalculate,
        s,
        rian,
        BulkSelection(expected_count=2, ids=[TE[2], ghost]),
    )
    assert error.code == "SAFETY_STOP"
    assert "the selection currently matches 1 entries, but you confirmed 2" in error.summary


def test_ids_mode_contains_view_scope(s, adi):
    """guardEntryIds parity: a manager posting someone else's row id
    hits the safety stop (the scope query filters it out)."""
    error = err(
        bulk_recalculate,
        s,
        adi,
        BulkSelection(expected_count=1, ids=[TE[6]]),  # rian's batch
    )
    assert error.code == "SAFETY_STOP"


# ------------------------------------------------------------ bulk edit


def test_bulk_edit_verified_counts_and_undo_restores_exactly(s, rian):
    before = all_snapshots(s)
    gary = gary_party_id(s)
    result = bulk_edit_fields(
        s,
        rian,
        BulkSelection(expected_count=2, filters=PENDING_PLUSROI),
        worker_party_id=gary,
    )
    assert result["matched"] == 2 and result["changed"] == 2
    assert result["skipped_locked"] == 0
    # only the scoped rows changed
    after = all_snapshots(s)
    for entry_id, snap in after.items():
        if entry_id in (str(TE[2]), str(TE[7])):
            assert snap["worker_party_id"] == str(gary)
            assert snap != before[entry_id]
        else:
            assert snap == before[entry_id]
    # undo: byte-identical restoration
    undo = undo_entries_action(s, rian, uuid.UUID(result["undo_id"]))
    assert undo["restored"] == 2 and undo["skipped_locked"] == 0
    assert all_snapshots(s) == before


def test_t_ent_25_manager_skips_locked_owner_includes(s, rian, adi):
    admin_project = s.scalars(select(Project).where(Project.name == "Admin")).one()
    result = bulk_edit_fields(
        s,
        adi,
        BulkSelection(expected_count=2, ids=[TE[1], TE[2]]),  # te-1 is invoice-locked
        project_id=admin_project.id,
    )
    assert result["matched"] == 2
    assert result["skipped_locked"] == 1
    assert entry(s, 1).project_id != admin_project.id  # locked row untouched
    assert entry(s, 2).project_id == admin_project.id

    owner = bulk_edit_fields(
        s,
        rian,
        BulkSelection(expected_count=1, ids=[TE[1]]),
        project_id=admin_project.id,
    )
    assert owner["skipped_locked"] == 0
    assert entry(s, 1).project_id == admin_project.id  # owner may (B64)


def test_bulk_edit_all_locked_409(s, adi):
    error = err(
        bulk_edit_fields,
        s,
        adi,
        BulkSelection(expected_count=1, ids=[TE[1]]),
        worker_party_id=gary_party_id(s),
    )
    assert error.summary == "No entries left to reassign after applying the lock guard."


def test_t_ent_24_quick_create_chain(s, rian):
    error = err(
        bulk_edit_fields,
        s,
        rian,
        BulkSelection(expected_count=1, ids=[TE[2]]),
        new_operator="PlusROI",
        new_client="Brand New Client",
        new_project="",
    )
    assert error.summary == (
        "To create a new project here, fill in all three: operator + client + project."
    )

    admin_project = s.scalars(select(Project).where(Project.name == "Admin")).one()
    result = bulk_edit_fields(
        s,
        rian,
        BulkSelection(expected_count=1, ids=[TE[2]]),
        project_id=admin_project.id,  # overridden by the chain (B37)
        new_operator="PlusROI",
        new_client="Brand New Client",
        new_project="Brand New Project",
    )
    assert result["changed"] == 1
    created = s.scalars(select(Project).where(Project.name == "Brand New Project")).one()
    assert entry(s, 2).project_id == created.id
    client = s.get(Party, created.client_party_id)
    assert client.name == "Brand New Client"


def test_bulk_edit_no_target_422(s, rian):
    error = err(
        bulk_edit_fields, s, rian, BulkSelection(expected_count=1, ids=[TE[2]])
    )
    assert error.summary == "Pick a project or a team member to reassign."


def test_bulk_edit_worker_restamps_at_new_project(s, rian):
    """T-ENT-23: project applies first, so the re-stamp resolves rates
    for the NEW project (Site Rebuild carries a project-wide -20% and
    an adi-specific absolute 20)."""
    adi_party = s.scalars(
        select(Party.id).where(Party.kind == "person", Party.email == "info@adipramono.com")
    ).one()
    site = s.scalars(select(Project).where(Project.name == "Site Rebuild")).one()
    result = bulk_edit_fields(
        s,
        rian,
        BulkSelection(expected_count=1, ids=[TE[3]]),  # gary's Admin row, 7200s
        worker_party_id=adi_party,
        project_id=site.id,
    )
    assert result["changed"] == 1
    row = entry(s, 3)
    assert row.project_id == site.id and row.worker_party_id == adi_party
    # adi on Site Rebuild: cost 14/h × 2h = 28; billout = worker-specific
    # absolute override 20/h × 2h = 40 (NOT the default 35 or pct rule)
    assert str(row.cost_amount) == "28.00"
    assert str(row.billout_amount) == "40.00"


# ------------------------------------------------------- bulk recalculate


def test_bulk_recalculate_skips_locked_for_everyone(s, rian):
    n = entry_totals(s, rian, EntryFilters())["row_count"]
    assert n == 7
    before = all_snapshots(s)
    result = bulk_recalculate(s, rian, BulkSelection(expected_count=7, filters=EntryFilters()))
    assert result["matched"] == 7
    assert result["skipped_locked"] == 2  # te-1 (invoice) + te-6 (legacy lock)
    # locked rows byte-identical (T-ADJ-017 — rebuild target)
    assert snapshot_entry(entry(s, 1)) == before[str(TE[1])]
    assert snapshot_entry(entry(s, 6)) == before[str(TE[6])]
    # the no-worker row keeps NULL stamps (T-ADJ-014 / T-MNY-001)
    unresolved = entry(s, 4)
    assert unresolved.worker_party_id is None
    assert unresolved.cost_amount is None and unresolved.billout_amount is None
    # honest counts: changed == rows whose values actually differ
    after = all_snapshots(s)
    truly_changed = sum(1 for k in after if after[k] != before[k])
    assert result["changed"] == truly_changed
    # and the whole thing undoes exactly
    undo = undo_entries_action(s, rian, uuid.UUID(result["undo_id"]))
    assert undo["restored"] == truly_changed
    assert all_snapshots(s) == before


# ----------------------------------------------------------- bulk delete


def test_bulk_delete_skips_locked_and_undo_reinserts(s, rian):
    before = all_snapshots(s)
    result = bulk_delete(
        s, rian, BulkSelection(expected_count=7, filters=EntryFilters())
    )
    assert result["matched"] == 7
    assert result["changed"] == 5  # locked te-1/te-6 skipped
    assert result["skipped_locked"] == 2
    remaining = s.scalars(select(TimeEntry.id)).all()
    assert set(remaining) == {TE[1], TE[6]}

    undo = undo_entries_action(s, rian, uuid.UUID(result["undo_id"]))
    assert undo["reinserted"] == 5
    assert all_snapshots(s) == before  # byte-identical, ids preserved


def test_undo_skips_row_locked_since_bulk_edit(s, rian):
    from app.models import Invoice, InvoiceLine

    result = bulk_edit_fields(
        s,
        rian,
        BulkSelection(expected_count=2, filters=PENDING_PLUSROI),
        worker_party_id=gary_party_id(s),
    )
    # te-2 gets attached to an invoice AFTER the bulk edit
    invoice = Invoice(tenant_id=entry(s, 2).tenant_id, name="Race INV", currency="CAD")
    s.add(invoice)
    s.flush()
    line = InvoiceLine(invoice_id=invoice.id, kind="time", amount=1)
    s.add(line)
    s.flush()
    entry(s, 2).invoice_line_id = line.id
    s.commit()

    undo = undo_entries_action(s, rian, uuid.UUID(result["undo_id"]))
    assert undo["restored"] == 1  # te-7 only
    assert undo["skipped_locked"] == 1  # te-2 — locked since, reported exactly
    assert entry(s, 2).worker_party_id == gary_party_id(s)  # untouched by undo
