import { describe, it, expect, afterEach } from "vitest";
import prisma from "../config/database";
import { createOffer, getOfferTotals } from "../services/offers.service";
import {
  createInvoice,
  getInvoiceListTotals,
} from "../services/invoices.service";
import { getReceivedInvoiceListTotals } from "../routes/admin/received-invoices";

// Per-currency total aggregation for the offers, issued-invoices and
// received-invoices lists. These hit the real `app_test` DB via the service
// layer (suite convention) and prove the totals fns sum each document's TOTAL
// per currency across the WHOLE filtered set, and that the filtering `where`
// (search / month-year / status) is respected — mirroring order-totals.test.ts.
//
// For invoices a far-future month/year is used so seeded/other-test rows can't
// fall in the window and skew the deterministic sums. Offers have NO date
// filter, so they are isolated by a unique `project_code` searched on instead.
// Received invoices store amount as GROSS (VAT-inclusive), summed directly.

const STATS_YEAR = 2097;
const STATS_MONTH = 8; // -> invoice issue_date window [2097-08-01, 2097-09-01)

// Unique marker so the offers test (which has no date filter) can scope its
// aggregation to only the rows it created via a `search` on project_code.
const OFFER_MARKER = `TOTALS-TEST-${STATS_YEAR}`;

const createdOfferIds: number[] = [];
const createdInvoiceIds: number[] = [];
const createdReceivedIds: number[] = [];

afterEach(async () => {
  for (const id of createdOfferIds)
    await prisma.quotations.deleteMany({ where: { id } });
  for (const id of createdInvoiceIds)
    await prisma.invoices.deleteMany({ where: { id } });
  for (const id of createdReceivedIds)
    await prisma.received_invoices.deleteMany({ where: { id } });
  createdOfferIds.length = 0;
  createdInvoiceIds.length = 0;
  createdReceivedIds.length = 0;
  // Finalized offers/invoices consume their sequences — reset so we don't leak
  // a consumed number into sibling test files.
  await prisma.number_sequences.deleteMany({
    where: { type: { in: ["offer", "invoice"] } },
  });
});

function amountFor(
  totals: { currency: string; amount: number }[],
  currency: string,
): number | undefined {
  return totals.find((t) => t.currency === currency)?.amount;
}

describe("getOfferTotals per-currency aggregation", () => {
  it("sums offer NET total per currency over the full filtered set", async () => {
    // Two CZK offers + one EUR. NET only (enrichQuotation math — offers are
    // not tax documents, no VAT anywhere).
    //   CZK #1: 2 x 1000 = 2000
    //   CZK #2: 1 x 500  =  500   => CZK total 2500
    //   EUR   : 3 x 100  =  300   => EUR total  300
    const mk = async (currency: string, qty: number, price: number) => {
      const res = await createOffer({
        status: "draft", // draft so no offer number is consumed
        currency,
        project_code: OFFER_MARKER,
        items: [{ description: "X", quantity: qty, unit_price: price }],
      });
      if ("error" in res) throw new Error(JSON.stringify(res));
      createdOfferIds.push(res.id);
      return res.id;
    };

    await mk("CZK", 2, 1000);
    await mk("CZK", 1, 500);
    await mk("EUR", 3, 100);

    const { totals } = await getOfferTotals({ search: OFFER_MARKER });
    expect(amountFor(totals, "CZK")).toBe(2500);
    expect(amountFor(totals, "EUR")).toBe(300);
  });

  it("respects the where: a status filter narrows the result", async () => {
    const mk = async (status: string) => {
      const res = await createOffer({
        status,
        currency: "CZK",
        project_code: OFFER_MARKER,
        items: [{ description: "X", quantity: 1, unit_price: 1000 }],
      });
      if ("error" in res) throw new Error(JSON.stringify(res));
      createdOfferIds.push(res.id);
      return res.id;
    };

    // Two drafts + one invalidated, all sharing the marker.
    await mk("draft");
    await mk("draft");
    await mk("invalidated");

    // Marker only: all three count -> 3 x 1000 = 3000 (net).
    const all = await getOfferTotals({ search: OFFER_MARKER });
    expect(amountFor(all.totals, "CZK")).toBe(3000);

    // Status filter narrows to the two drafts -> 2 x 1000 = 2000.
    const onlyDraft = await getOfferTotals({
      search: OFFER_MARKER,
      status: "draft",
    });
    expect(amountFor(onlyDraft.totals, "CZK")).toBe(2000);

    // Invalidated alone -> 1000.
    const onlyInvalid = await getOfferTotals({
      search: OFFER_MARKER,
      status: "invalidated",
    });
    expect(amountFor(onlyInvalid.totals, "CZK")).toBe(1000);
  });
});

describe("getInvoiceListTotals (issued invoices) per-currency aggregation", () => {
  // issue_date is settable at create, so no post-create pin needed. VAT is
  // applied per-line (computeInvoiceTotals) but with a single line per invoice
  // here the result matches the whole-subtotal math.
  const dateStr = `${STATS_YEAR}-0${STATS_MONTH}-15`;

  const mk = async (
    currency: string,
    qty: number,
    price: number,
    status = "draft",
  ) => {
    const inv = await createInvoice({
      status,
      currency,
      vat_rate: 21,
      apply_vat: true,
      issue_date: dateStr,
      items: [{ description: "X", quantity: qty, unit_price: price }],
    });
    createdInvoiceIds.push(inv.id);
    return inv.id;
  };

  it("sums invoice TOTAL incl. VAT per currency over the full filtered set", async () => {
    //   CZK #1: 2 x 1000 @21% = 2420
    //   CZK #2: 1 x 500  @21% =  605   => CZK total 3025
    //   EUR   : 3 x 100  @21% =  363   => EUR total  363
    await mk("CZK", 2, 1000);
    await mk("CZK", 1, 500);
    await mk("EUR", 3, 100);

    const { totals } = await getInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
    });
    expect(amountFor(totals, "CZK")).toBe(3025);
    expect(amountFor(totals, "EUR")).toBe(363);
  });

  it("respects the where: a different month and a status filter change the result", async () => {
    const nextDateStr = `${STATS_YEAR}-0${STATS_MONTH + 1}-15`;

    // Target month: one draft, one issued (both 1210). Next month: one draft.
    await mk("CZK", 1, 1000, "draft");
    // An already-issued invoice numbers immediately; that's fine for the sum.
    const issued = await createInvoice({
      status: "issued",
      currency: "CZK",
      vat_rate: 21,
      apply_vat: true,
      issue_date: dateStr,
      items: [{ description: "X", quantity: 1, unit_price: 1000 }],
    });
    createdInvoiceIds.push(issued.id);

    const next = await createInvoice({
      status: "draft",
      currency: "CZK",
      vat_rate: 21,
      apply_vat: true,
      issue_date: nextDateStr,
      items: [{ description: "X", quantity: 1, unit_price: 1000 }],
    });
    createdInvoiceIds.push(next.id);

    // Whole target month: both count -> 2 x 1210 = 2420.
    const whole = await getInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
    });
    expect(amountFor(whole.totals, "CZK")).toBe(2420);

    // Status filter narrows to the single 'issued' in the target month -> 1210.
    const onlyIssued = await getInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
      status: "issued",
    });
    expect(amountFor(onlyIssued.totals, "CZK")).toBe(1210);

    // Next month sees only the one invoice there -> 1210.
    const nextMonth = await getInvoiceListTotals({
      month: STATS_MONTH + 1,
      year: STATS_YEAR,
    });
    expect(amountFor(nextMonth.totals, "CZK")).toBe(1210);
  });
});

describe("received-invoices list-totals per-currency aggregation (GROSS)", () => {
  // No service create for received invoices — rows are written directly. The
  // list/totals `where` filters by the month/year COLUMNS (not issue_date).
  // amount is GROSS, summed directly per currency.
  const mkReceived = async (currency: string, amount: number) => {
    const row = await prisma.received_invoices.create({
      data: {
        month: STATS_MONTH,
        year: STATS_YEAR,
        supplier_name: "Totals Test Supplier",
        amount,
        currency,
        vat_rate: 21,
        status: "unpaid",
      },
    });
    createdReceivedIds.push(row.id);
    return row.id;
  };

  it("sums GROSS amount per currency over the full filtered set", async () => {
    // Two CZK (2420 + 605) + one EUR (363). Amounts are GROSS — summed as-is.
    await mkReceived("CZK", 2420);
    await mkReceived("CZK", 605);
    await mkReceived("EUR", 363);

    const { totals } = await getReceivedInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
    });
    expect(amountFor(totals, "CZK")).toBe(3025);
    expect(amountFor(totals, "EUR")).toBe(363);
  });

  it("respects the where: a different month and a search filter change the result", async () => {
    await mkReceived("CZK", 1000);
    // A row in the NEXT month must not count toward the target month's total.
    const other = await prisma.received_invoices.create({
      data: {
        month: STATS_MONTH + 1,
        year: STATS_YEAR,
        supplier_name: "Totals Test Supplier",
        amount: 9999,
        currency: "CZK",
        vat_rate: 21,
        status: "unpaid",
      },
    });
    createdReceivedIds.push(other.id);

    const { totals } = await getReceivedInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
    });
    expect(amountFor(totals, "CZK")).toBe(1000);

    // Search on the supplier name still scopes to the target-month row.
    const { totals: searched } = await getReceivedInvoiceListTotals({
      month: STATS_MONTH,
      year: STATS_YEAR,
      search: "Totals Test Supplier",
    });
    expect(amountFor(searched, "CZK")).toBe(1000);
  });
});
