import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  EmptyJournalEntryChangesError,
  InvalidDebitCreditError,
  InvalidEntryDateError,
  InvalidStatusForUpdateError,
  JournalEntryNotFoundError,
  JournalLineNotFoundError,
  MinimumLinesNotMetError,
} from "../lib/errors.generated";
import type { CoaManagementQueries } from "../module";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccount,
  baseAccount2,
  baseJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  cancelledJournalEntry,
  inactiveAccount,
  postedJournalEntry,
} from "../testing/fixtures";
import { run as runBase, type JournalLineInput } from "./updateJournalEntry";

const newLine: JournalLineInput = {
  accountId: baseAccount.id,
  debitAmount: "250",
  creditAmount: null,
  description: "Added debit line",
};

const currentLines = [baseJournalLine, baseJournalLine2];
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("updateJournalEntry", () => {
  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: "missing-entry", headerPatch: { description: "Updated" } }, commandCtx),
    );

    expectErr(result, JournalEntryNotFoundError);
  });

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

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

    expectErr(result, InvalidStatusForUpdateError);
  });

  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, headerPatch: { description: "Updated" } },
        commandCtx,
      ),
    );

    expectErr(result, InvalidStatusForUpdateError);
  });

  it("returns error when neither headerPatch nor line changes are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry]);

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

    expectErr(result, EmptyJournalEntryChangesError);
  });

  it("updates description on a DRAFT journal entry without touching lines", async () => {
    const updatedEntry = { ...baseJournalEntry, description: "New description" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines, [updatedEntry]);

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

    const value = expectOk(result);
    expect(value.journalEntry.description).toBe("New description");
    expect(value).not.toHaveProperty("journalLines");
    expect(mock.deletes).toHaveLength(0);
    expect(mock.inserts).toHaveLength(0);
  });

  it("updates source document on a DRAFT journal entry", async () => {
    const sourceDocumentId = "550e8400-e29b-41d4-a716-446655440001";
    const updatedEntry = {
      ...baseJournalEntry,
      sourceDocumentType: "PURCHASE_BILL",
      sourceDocumentId,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines, [updatedEntry]);

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

    const value = expectOk(result);
    expect(value.journalEntry.sourceDocumentType).toBe("PURCHASE_BILL");
    expect(value.journalEntry.sourceDocumentId).toBe(sourceDocumentId);
  });

  it("updates entry date on a DRAFT journal entry", async () => {
    const entryDate = new Date("2024-01-31");
    const updatedEntry = { ...baseJournalEntry, entryDate };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines, [updatedEntry]);

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

    const value = expectOk(result);
    expect(value.journalEntry.entryDate).toBe(entryDate);
  });

  it("returns error when headerPatch entryDate is invalid", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: baseJournalEntry.id, headerPatch: { entryDate: new Date("invalid-date") } },
        commandCtx,
      ),
    );

    expectErr(result, InvalidEntryDateError);
  });

  it("adds a new line without touching existing lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

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

    expectOk(result);
    expect(mock.deletes).toHaveLength(0);
    expect(mock.updates).toHaveLength(0);
    expect(mock.inserts).toHaveLength(1);
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({ journalEntryId: baseJournalEntry.id, accountId: baseAccount.id }),
    ]);
  });

  it("updates a line in place", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseJournalEntry.id,
          updateLines: [{ lineId: baseJournalLine.id, patch: { debitAmount: "1200" } }],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.deletes).toHaveLength(0);
    expect(mock.inserts).toHaveLength(0);
    expect(mock.updates).toHaveLength(1);
    expect(mock.updates[0].updateValues()).toEqual({ debitAmount: "1200" });
  });

  it("removes a line while preserving remaining lines", async () => {
    const thirdLine = { ...baseJournalLine, id: "journal-line-3", debitAmount: "500" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], [...currentLines, thirdLine]);

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

    expectOk(result);
    expect(mock.deletes).toHaveLength(1);
  });

  it("returns error when a line edit references a missing line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseJournalEntry.id,
          updateLines: [{ lineId: "missing-line", patch: { debitAmount: "1" } }],
        },
        commandCtx,
      ),
    );

    expectErr(result, JournalLineNotFoundError);
  });

  it("returns error when effective lines would drop below two", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

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

    expectErr(result, MinimumLinesNotMetError);
  });

  it("returns error when an added or updated line account does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: baseJournalEntry.id, addLines: [{ ...newLine, accountId: "missing-account" }] },
        commandCtx,
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when an added or updated line account is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: baseJournalEntry.id, addLines: [{ ...newLine, accountId: inactiveAccount.id }] },
        commandCtx,
      ),
    );

    expectErr(result, AccountInactiveError);
  });

  it("returns error when an added or updated line has invalid debit and credit values", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseJournalEntry.id,
          updateLines: [{ lineId: baseJournalLine.id, patch: { creditAmount: "1000" } }],
        },
        commandCtx,
      ),
    );

    expectErr(result, InvalidDebitCreditError);
  });

  it("passes custom header and line fields through incremental changes", async () => {
    const updatedEntry = { ...baseJournalEntry, localVoucherCode: "JV-002" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseJournalEntry], currentLines, [updatedEntry]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseJournalEntry.id,
          headerPatch: { localVoucherCode: "JV-002" },
          addLines: [{ ...newLine, costCenterId: "cc-new" }],
          updateLines: [{ lineId: baseJournalLine.id, patch: { costCenterId: "cc-updated" } }],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.updates[0].updateValues()).toEqual({ localVoucherCode: "JV-002" });
    expect(mock.updates[1].updateValues()).toEqual({ costCenterId: "cc-updated" });
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({ costCenterId: "cc-new" }),
    ]);
  });
});
