import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  JournalEntryNotFoundError,
  InvalidStatusForPostingError,
  MinimumLinesNotMetError,
  UnbalancedEntryError,
  InvalidPeriodStatusError,
  AccountNotFoundError,
} from "../lib/errors.generated";
import type { CoaManagementQueries } from "../module";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseJournalEntry,
  cancelledJournalEntry,
  postedJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  baseAccountingPeriod,
  neverOpenedPeriod,
  closedPeriod,
  permanentlyClosedPeriod,
  baseAccount,
  baseAccount2,
  inactiveAccount,
} from "../testing/fixtures";
import { run as runBase } from "./postJournalEntry";

const defaultAccounts = [baseAccount, baseAccount2, inactiveAccount];

function createCoaManagementQueries(accounts = defaultAccounts) {
  type ListAccounts = CoaManagementQueries["listAccounts"];
  return {
    listAccounts: (_db: Parameters<ListAccounts>[0], input: Parameters<ListAccounts>[1]) =>
      Promise.resolve({
        ok: true as const,
        value: {
          items: accounts
            .filter((account) => !input.accountIds || input.accountIds.includes(account.id))
            .map((account) => ({
              id: account.id,
              code: account.code,
              name: account.name,
              accountType: "ASSET" as const,
              companyId: baseJournalEntry.companyId,
              status: account.status as "ACTIVE" | "INACTIVE",
              createdAt: new Date("2024-01-01T00:00:00.000Z"),
              updatedAt: null,
            })),
          hasNextPage: false,
        },
      }),
  } satisfies Pick<CoaManagementQueries, "listAccounts">;
}

const run = (
  db: Parameters<typeof runBase>[0],
  input: unknown,
  ctx: Parameters<typeof runBase>[2],
  accounts = defaultAccounts,
) => runBase(db, input as Parameters<typeof runBase>[1], ctx, createCoaManagementQueries(accounts));

describe("postJournalEntry", () => {
  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 already POSTED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedJournalEntry]);

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

    expectErr(result, InvalidStatusForPostingError);
  });

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

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

    expectErr(result, InvalidStatusForPostingError);
  });

  it("returns error when entry has fewer than two journal lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], [baseJournalLine]);

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

    expectErr(result, MinimumLinesNotMetError);
  });

  it("returns error when entry has only one line (debit only or credit only)", async () => {
    const singleLine = { ...baseJournalLine };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], [singleLine]);

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

    expectErr(result, MinimumLinesNotMetError);
  });

  it("returns error when total debits do not equal total credits", async () => {
    const unbalancedCreditLine = {
      ...baseJournalLine2,
      creditAmount: "500",
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [baseJournalLine, unbalancedCreditLine],
      [baseAccountingPeriod],
    );

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

    expectErr(result, UnbalancedEntryError);
  });

  it("returns error when target period is in NEVER_OPENED status", async () => {
    const entryInNeverOpened = {
      ...baseJournalEntry,
      accountingPeriodId: neverOpenedPeriod.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [entryInNeverOpened],
      [baseJournalLine, baseJournalLine2],
      [neverOpenedPeriod],
    );

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when target period is in CLOSED status", async () => {
    const entryInClosed = {
      ...baseJournalEntry,
      accountingPeriodId: closedPeriod.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([entryInClosed], [baseJournalLine, baseJournalLine2], [closedPeriod]);

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when target period is in PERMANENTLY_CLOSED status", async () => {
    const entryInPermClosed = {
      ...baseJournalEntry,
      accountingPeriodId: permanentlyClosedPeriod.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [entryInPermClosed],
      [baseJournalLine, baseJournalLine2],
      [permanentlyClosedPeriod],
    );

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when a GL account reference does not exist", async () => {
    const lineWithMissingAccount = {
      ...baseJournalLine,
      accountId: "missing-account",
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [lineWithMissingAccount, baseJournalLine2],
      [baseAccountingPeriod],
    );

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

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when a GL account reference is inactive", async () => {
    const lineWithInactiveAccount = {
      ...baseJournalLine,
      accountId: inactiveAccount.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [lineWithInactiveAccount, baseJournalLine2],
      [baseAccountingPeriod],
    );

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

    expectErr(result, AccountInactiveError);
  });

  it("posts balanced DRAFT entry to OPEN period", async () => {
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [baseJournalLine, baseJournalLine2],
      [baseAccountingPeriod],
      [postedEntry],
    );

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

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

  it("does not copy journal lines into a separate ledger table when posting succeeds", async () => {
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [baseJournalLine, baseJournalLine2],
      [baseAccountingPeriod],
      [postedEntry],
    );

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

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

  it("entry becomes immutable after posting", async () => {
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseJournalEntry],
      [baseJournalLine, baseJournalLine2],
      [baseAccountingPeriod],
      [postedEntry],
    );

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

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