import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForReversalError,
  AlreadyReversedError,
  InvalidPeriodStatusError,
} from "../lib/errors.generated";
import {
  baseJournalEntry,
  postedJournalEntry,
  reversedJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  baseAccountingPeriod,
  futureEnterablePeriod,
  neverOpenedPeriod,
  closedPeriod,
  permanentlyClosedPeriod,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./reverseJournalEntry";

const postedLines = [
  { ...baseJournalLine, journalEntryId: postedJournalEntry.id },
  { ...baseJournalLine2, journalEntryId: postedJournalEntry.id },
];

describe("reverseJournalEntry", () => {
  it("returns error when journal entry does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { id: "nonexistent" }, commandCtx);

    expectErr(result, JournalEntryNotFoundError);
  });

  it("returns error when journal entry is in DRAFT status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseJournalEntry); // DRAFT entry

    const result = await run(db, { id: baseJournalEntry.id }, commandCtx);

    expectErr(result, InvalidStatusForReversalError);
  });

  it("returns error when journal entry is already REVERSED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(reversedJournalEntry);

    const result = await run(db, { id: reversedJournalEntry.id }, commandCtx);

    expectErr(result, AlreadyReversedError);
  });

  it("returns error when reversal period is in NEVER_OPENED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(neverOpenedPeriod); // period lookup

    const result = await run(
      db,
      { id: postedJournalEntry.id, reversalPeriodId: neverOpenedPeriod.id },
      commandCtx,
    );

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when reversal period is in CLOSED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(closedPeriod); // period lookup

    const result = await run(
      db,
      { id: postedJournalEntry.id, reversalPeriodId: closedPeriod.id },
      commandCtx,
    );

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when reversal period is in PERMANENTLY_CLOSED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(permanentlyClosedPeriod); // period lookup

    const result = await run(
      db,
      { id: postedJournalEntry.id, reversalPeriodId: permanentlyClosedPeriod.id },
      commandCtx,
    );

    expectErr(result, InvalidPeriodStatusError);
  });

  it("creates mirror entry with all debit/credit amounts inverted", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => [
        // insert reversal lines
        {
          ...postedLines[0],
          debitAmount: null,
          creditAmount: 1000,
          functionalDebitAmount: null,
          functionalCreditAmount: 1000,
        },
        {
          ...postedLines[1],
          debitAmount: 1000,
          creditAmount: null,
          functionalDebitAmount: 1000,
          functionalCreditAmount: null,
        },
      ]);
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    expectOk(result);
    // Verify values were called with inverted amounts
    expect(spies.values).toHaveBeenCalled();
  });

  it("reversal entry references the original entry", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    const value = expectOk(result);
    expect(value.reversalEntry.reversalOfId).toBe(postedJournalEntry.id);
  });

  it("reversal entry is automatically posted", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
      postedAt: new Date(),
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    const value = expectOk(result);
    expect(value.reversalEntry.status).toBe("POSTED");
    expect(value.reversalEntry.postedAt).toBeDefined();
  });

  it("original entry transitions from POSTED to REVERSED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    const value = expectOk(result);
    expect(value.originalEntry.status).toBe("REVERSED");
  });

  it("reversal entry targets the specified accounting period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
      accountingPeriodId: futureEnterablePeriod.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(futureEnterablePeriod); // period lookup (FUTURE_ENTERABLE)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(
      db,
      { id: postedJournalEntry.id, reversalPeriodId: futureEnterablePeriod.id },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.reversalEntry.accountingPeriodId).toBe(futureEnterablePeriod.id);
  });

  it("multi-currency lines are reversed with same currency and exchange rate", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const multiCurrencyLines = [
      {
        ...postedLines[0],
        currencyCode: "currency-1",
        exchangeRate: 1.5,
        debitAmount: 1500,
        functionalDebitAmount: 1000,
      },
      {
        ...postedLines[1],
        currencyCode: "currency-1",
        exchangeRate: 1.5,
        creditAmount: 1500,
        functionalCreditAmount: 1000,
      },
    ];
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => multiCurrencyLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    expectOk(result);
    // Verify that values() was called with reversed amounts preserving currency info
    expect(spies.values).toHaveBeenCalled();
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const insertedLines = spies.values.mock.calls[1]?.[0];
    if (insertedLines) {
      for (const line of insertedLines as { currencyCode: string; exchangeRate: number }[]) {
        expect(line.currencyCode).toBe("currency-1");
        expect(line.exchangeRate).toBe(1.5);
      }
    }
  });

  it("emits audit event recording status transition, reversal entry reference, and acting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };
    const reversedOriginal = { ...postedJournalEntry, status: "REVERSED" };

    spies.select
      .mockReturnValueOnce(postedJournalEntry) // entry lookup
      .mockImplementationOnce(() => postedLines) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup (OPEN)
    spies.insert
      .mockReturnValueOnce(reversalEntry) // insert reversal entry
      .mockImplementationOnce(() => []); // insert reversal lines
    spies.update.mockReturnValueOnce(reversedOriginal); // update original entry

    const result = await run(db, { id: postedJournalEntry.id }, commandCtx);

    const value = expectOk(result);
    expect(value.auditTrail).toBeDefined();
    expect(value.auditTrail).toContainEqual(
      expect.objectContaining({
        type: "STATUS_TRANSITION",
        from: "POSTED",
        to: "REVERSED",
        actorId: commandCtx.actorId,
      }),
    );
  });
});
