import { describe, it, expect } from "vitest";
import {
  applyMapping,
  inFileDuplicates,
  last4,
  splitByCoverage,
  type ColumnMapping,
  type ParsedTransaction,
} from "@/lib/csv-parser";

/**
 * The statement-coverage rule, which replaced a whole Frollo-specific importer.
 *
 * The first aggregator import wrote 422 duplicate rows out of 550 — $1,023,824
 * of movement counted twice — because "these accounts issue no statements" was
 * asserted rather than queried. Amount-matching could not fix it either: the two
 * sources decompose the same event differently, bundling a transfer fee where
 * the statement itemises it.
 *
 * A statement's end date answers the question that actually has an answer: up
 * to what date is this account complete? On the real 2026-08-13 export it takes
 * 2,607 rows down to 171, with no account allowlist and no card exclusions.
 */

const tx = (o: Partial<ParsedTransaction> & { date: string }): ParsedTransaction => ({
  description: "SOMETHING",
  amount: 10,
  transaction_type: "debit",
  ...o,
});

describe("last4", () => {
  it("reads the same account written four different ways", () => {
    // Every one of these forms appears in the real data.
    expect(last4("xxxxxxxxxxxx2176")).toBe("2176");
    expect(last4("235242176")).toBe("2176");
    expect(last4("xxx-xxx xx4878")).toBe("4878");
    expect(last4("4085-56264")).toBe("6264");
  });

  it("returns empty for an unusable identifier rather than guessing", () => {
    expect(last4("N/A")).toBe("");
    expect(last4("12")).toBe("");
    expect(last4("")).toBe("");
  });
});

describe("splitByCoverage", () => {
  const coverage = [{ last4: "2176", coveredTo: "2026-07-26" }];

  it("excludes a row on or before the account's newest statement", () => {
    const { keep, covered } = splitByCoverage(
      [tx({ date: "2026-07-25", account: "xxxxxxxxxxxx2176" })],
      coverage
    );
    expect(covered).toHaveLength(1);
    expect(keep).toHaveLength(0);
  });

  it("treats the watermark date itself as covered", () => {
    const { covered } = splitByCoverage(
      [tx({ date: "2026-07-26", account: "xxxxxxxxxxxx2176" })],
      coverage
    );
    expect(covered).toHaveLength(1);
  });

  it("keeps a row after the watermark — the whole point of the feed", () => {
    // The 2026-08-12 salary: the statement has not arrived, so this row is the
    // only record of it and must survive.
    const { keep } = splitByCoverage(
      [tx({ date: "2026-08-12", account: "xxxxxxxxxxxx2176", amount: 10782 })],
      coverage
    );
    expect(keep).toHaveLength(1);
  });

  it("keeps everything for an account with no statements", () => {
    const { keep, covered } = splitByCoverage(
      [tx({ date: "2020-01-01", account: "xxx-xxx xx4878" })],
      coverage
    );
    expect(keep).toHaveLength(1);
    expect(covered).toHaveLength(0);
  });

  it("keeps everything when no account column was mapped", () => {
    // A single-account bank export has no account column, and a row must never
    // be dropped on a guess about which account it belongs to.
    const { keep, covered } = splitByCoverage([tx({ date: "2020-01-01" })], coverage);
    expect(keep).toHaveLength(1);
    expect(covered).toHaveLength(0);
  });

  it("applies each account's own watermark, not a global one", () => {
    // The real export spans watermarks from 2026-03-31 to 2026-07-26. A single
    // date would either import duplicates or discard real rows.
    const { keep, covered } = splitByCoverage(
      [
        tx({ date: "2026-05-01", account: "xxxx0887" }), // covered to 2026-03-31 → keep
        tx({ date: "2026-05-01", account: "xxxx2176" }), // covered to 2026-07-26 → drop
      ],
      [
        { last4: "0887", coveredTo: "2026-03-31" },
        { last4: "2176", coveredTo: "2026-07-26" },
      ]
    );
    expect(keep).toHaveLength(1);
    expect(keep[0].account).toBe("xxxx0887");
    expect(covered).toHaveLength(1);
  });
});

describe("inFileDuplicates", () => {
  it("counts twins that a row id cannot catch", () => {
    // A CDR re-consent re-exports an account's whole history under fresh ids
    // while the originals survive, so source_ref sees two distinct rows. 385 of
    // these were in one 2,563-row export and doubled every salary payment.
    const rows = [
      tx({ date: "2026-07-07", amount: 10782, description: "HDR", account: "4830" }),
      tx({ date: "2026-07-07", amount: 10782, description: "HDR", account: "4830" }),
    ];
    expect(inFileDuplicates(rows)).toBe(1);
  });

  it("does not flag the same amount on different days", () => {
    expect(
      inFileDuplicates([
        tx({ date: "2026-07-07", amount: 5, description: "COFFEE" }),
        tx({ date: "2026-07-08", amount: 5, description: "COFFEE" }),
      ])
    ).toBe(0);
  });

  it("does not flag the same amount on different accounts", () => {
    // Both legs of an internal transfer: same day, same amount, different
    // accounts, and both are real.
    expect(
      inFileDuplicates([
        tx({ date: "2026-05-18", amount: 3076.04, description: "Transfer", account: "6264" }),
        tx({ date: "2026-05-18", amount: 3076.04, description: "Transfer", account: "9940" }),
      ])
    ).toBe(0);
  });
});

describe("applyMapping with account and row-id columns", () => {
  const labels = ["transaction_id", "description", "amount", "transaction_date", "account_number"];
  const mapping: ColumnMapping = {
    dateCol: "transaction_date",
    descriptionCol: "description",
    amountMode: "single",
    amountCol: "amount",
    accountCol: "account_number",
    sourceRefCol: "transaction_id",
  };

  it("carries the account and row id through", () => {
    const out = applyMapping(
      [["1326131276", "HDR Global Services", "10782.00", "2026-08-12", "xxxxxxxxxxxx4830"]],
      labels,
      mapping,
      "YYYY-MM-DD"
    );
    expect(out).toHaveLength(1);
    expect(out[0].account).toBe("xxxxxxxxxxxx4830");
    expect(out[0].source_ref).toBe("1326131276");
    // A positive single-column amount is money in.
    expect(out[0].transaction_type).toBe("credit");
  });

  it("marks a non-AUD row as native and unconverted", () => {
    // The 2026-08-12 salary is USD. Stored without this it reads as A$10,782 —
    // about a third under — and sits in a column of AUD figures looking normal.
    const out = applyMapping(
      [["1", "HDR Global Services", "10782.00", "2026-08-12", "xxxx4830", "USD"]],
      [...labels, "currency"],
      { ...mapping, currencyCol: "currency" },
      "YYYY-MM-DD"
    );
    expect(out[0].foreign_currency_code).toBe("USD");
    expect(out[0].foreign_currency_amount).toBe(10782);
  });

  it("leaves an AUD row unmarked", () => {
    const out = applyMapping(
      [["1", "SOMETHING", "-42.50", "2026-08-12", "xxxx4830", "AUD"]],
      [...labels, "currency"],
      { ...mapping, currencyCol: "currency" },
      "YYYY-MM-DD"
    );
    expect(out[0].foreign_currency_code).toBeUndefined();
  });

  it("ignores a currency cell that is not a code", () => {
    const out = applyMapping(
      [["1", "SOMETHING", "-42.50", "2026-08-12", "xxxx4830", ""]],
      [...labels, "currency"],
      { ...mapping, currencyCol: "currency" },
      "YYYY-MM-DD"
    );
    expect(out[0].foreign_currency_code).toBeUndefined();
  });

  it("leaves both undefined when the columns are not mapped", () => {
    const out = applyMapping(
      [["1", "SOMETHING", "-42.50", "2026-08-12", "xxxx4830"]],
      labels,
      { dateCol: "transaction_date", descriptionCol: "description", amountMode: "single", amountCol: "amount" },
      "YYYY-MM-DD"
    );
    expect(out[0].account).toBeUndefined();
    expect(out[0].source_ref).toBeUndefined();
    expect(out[0].transaction_type).toBe("debit");
  });
});
