import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForReversalError,
  AlreadyReversedError,
  InvalidPeriodStatusError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseJournalEntry,
  cancelledJournalEntry,
  postedJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  baseAccountingPeriod,
  neverOpenedPeriod,
  closedPeriod,
  permanentlyClosedPeriod,
} from "../testing/fixtures";
import { run } from "./reverseJournalEntry";

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

const reversalPostedLines = [
  {
    ...postedLines[0],
    id: "reversal-line-1",
    debitAmount: null,
    creditAmount: "1000",
  },
  {
    ...postedLines[1],
    id: "reversal-line-2",
    debitAmount: "1000",
    creditAmount: null,
  },
];

describe("reverseJournalEntry", () => {
  it("returns error when journal entry does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, { id: "nonexistent" }, commandCtx));

    expectErr(result, JournalEntryNotFoundError);
  });

  it("returns error when journal entry is in DRAFT status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry]); // DRAFT entry

    const result = await mock.withTx((trx) => run(trx, { id: baseJournalEntry.id }, commandCtx));

    expectErr(result, InvalidStatusForReversalError);
  });

  it("returns error when journal entry is in CANCELLED status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([cancelledJournalEntry]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: cancelledJournalEntry.id }, commandCtx),
    );

    expectErr(result, InvalidStatusForReversalError);
  });

  it("returns error when journal entry already has a reversal entry", async () => {
    const existingReversal = {
      ...postedJournalEntry,
      id: "existing-reversal-entry",
      reversalOfId: postedJournalEntry.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedJournalEntry], [existingReversal]);

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

    expectErr(result, AlreadyReversedError);
  });

  it("returns error when reversal period is in NEVER_OPENED status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedJournalEntry], [], postedLines, [neverOpenedPeriod]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: postedJournalEntry.id, reversalPeriodId: neverOpenedPeriod.id }, commandCtx),
    );

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when reversal period is in CLOSED status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedJournalEntry], [], postedLines, [closedPeriod]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: postedJournalEntry.id, reversalPeriodId: closedPeriod.id }, commandCtx),
    );

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when reversal period is in PERMANENTLY_CLOSED status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedJournalEntry], [], postedLines, [permanentlyClosedPeriod]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: postedJournalEntry.id, reversalPeriodId: permanentlyClosedPeriod.id },
        commandCtx,
      ),
    );

    expectErr(result, InvalidPeriodStatusError);
  });

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

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [baseAccountingPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

    expectOk(result);
    // Verify lines were inserted with inverted amounts
    expect(mock.inserts.length).toBeGreaterThan(0);
    expect(mock.inserts[1].insertRows()).toEqual([
      expect.objectContaining({
        journalEntryId: reversalEntry.id,
        debitAmount: null,
        creditAmount: "1000",
      }),
      expect.objectContaining({
        journalEntryId: reversalEntry.id,
        debitAmount: "1000",
        creditAmount: null,
      }),
    ]);
  });

  it("does not create separate ledger rows for the reversal entry", async () => {
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [baseAccountingPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

    expectOk(result);
    expect(mock.inserts).toHaveLength(2);
  });

  it("reversal entry references the original entry", async () => {
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [baseAccountingPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

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

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

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [baseAccountingPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

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

  it("original entry remains POSTED after reversal", async () => {
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
    };

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [baseAccountingPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) => run(trx, { id: postedJournalEntry.id }, commandCtx));

    const value = expectOk(result);
    expect(value.originalEntry.status).toBe("POSTED");
    expect(mock.updates).toHaveLength(0);
  });

  it("reversal entry targets the specified accounting period", async () => {
    const targetPeriod = {
      ...baseAccountingPeriod,
      id: "period-reversal",
    };
    const reversalEntry = {
      ...postedJournalEntry,
      id: "reversal-entry-1",
      status: "POSTED",
      reversalOfId: postedJournalEntry.id,
      accountingPeriodId: targetPeriod.id,
    };

    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedJournalEntry],
      [],
      postedLines,
      [targetPeriod],
      [reversalEntry],
      reversalPostedLines,
    );

    const result = await mock.withTx((trx) =>
      run(trx, { id: postedJournalEntry.id, reversalPeriodId: targetPeriod.id }, commandCtx),
    );

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