import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  AccountingPeriodCompanyMismatchError,
  AccountingPeriodNotFoundError,
  CompanyNotFoundError,
  InvalidDebitCreditError,
  InvalidEntryDateError,
  MinimumLinesNotMetError,
} from "../lib/errors.generated";
import type { CoaManagementQueries } from "../module";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccount,
  baseAccount2,
  baseAccountingPeriod,
  baseCompany,
  baseJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  inactiveAccount,
} from "../testing/fixtures";
import { mockOrgQueries, type Company } from "../testing/moduleMocks";
import {
  run as runBase,
  type CreateJournalEntryInput,
  type JournalLineInput,
} from "./createJournalEntry";

const header: CreateJournalEntryInput["header"] = {
  companyId: baseCompany.id,
  accountingPeriodId: baseAccountingPeriod.id,
  entryDate: new Date("2024-01-15"),
};

const lines: JournalLineInput[] = [
  {
    accountId: baseAccount.id,
    debitAmount: "1000",
    creditAmount: null,
    description: "Debit line",
  },
  {
    accountId: baseAccount2.id,
    debitAmount: null,
    creditAmount: "1000",
    description: "Credit line",
  },
];

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: baseCompany.id,
              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],
  company: Company | null = baseCompany,
  accounts = defaultAccounts,
) =>
  runBase(
    db,
    input as Parameters<typeof runBase>[1],
    ctx,
    mockOrgQueries(company),
    createCoaManagementQueries(accounts),
  );

function enqueueValidCreate(
  mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>,
  journalEntry: Record<string, unknown> = baseJournalEntry,
) {
  mock.enqueueResults([baseAccountingPeriod], [journalEntry], [baseJournalLine, baseJournalLine2]);
}

describe("createJournalEntry", () => {
  it("returns error when company does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(trx, { header: { ...header, companyId: "missing-company" }, lines }, commandCtx, null),
    );

    expectErr(result, CompanyNotFoundError);
  });

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

    const result = await mock.withTx((trx) =>
      run(trx, { header: { ...header, accountingPeriodId: "missing-period" }, lines }, commandCtx),
    );

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns error when accounting period belongs to a different company", async () => {
    const companyB = { id: "company-2" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAccountingPeriod]);

    const result = await mock.withTx((trx) =>
      run(trx, { header: { ...header, companyId: companyB.id }, lines }, commandCtx),
    );

    expectErr(result, AccountingPeriodCompanyMismatchError);
  });

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

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

    expectErr(result, InvalidEntryDateError);
  });

  it("returns error when initial lines are missing", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAccountingPeriod]);

    const result = await mock.withTx((trx) => run(trx, { header, lines: [] }, commandCtx));

    expectErr(result, MinimumLinesNotMetError);
  });

  it("creates journal entry in DRAFT status with required fields and lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock);

    const result = await mock.withTx((trx) => run(trx, { header, lines }, commandCtx));

    const value = expectOk(result);
    expect(value.journalEntry.status).toBe("DRAFT");
    expect(value).not.toHaveProperty("journalLines");
    expect(mock.inserts).toHaveLength(2);
  });

  it("creates journal entry with optional description", async () => {
    const entryWithDescription = {
      ...baseJournalEntry,
      description: "Monthly closing",
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock, entryWithDescription);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: {
            ...header,
            description: "Monthly closing",
          },
          lines,
        },
        commandCtx,
      ),
    );

    const value = expectOk(result);
    expect(value.journalEntry.description).toBe("Monthly closing");
  });

  it("creates journal entry with optional source document", async () => {
    const sourceDocumentId = "550e8400-e29b-41d4-a716-446655440000";
    const entryWithHeaderFields = {
      ...baseJournalEntry,
      sourceDocumentType: "SALES_INVOICE",
      sourceDocumentId,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock, entryWithHeaderFields);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: {
            ...header,
            sourceDocumentType: "SALES_INVOICE",
            sourceDocumentId,
          },
          lines,
        },
        commandCtx,
      ),
    );

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

  it("passes custom header and line fields through to inserts", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: {
            ...header,
            localVoucherCode: "JV-001",
          },
          lines: [
            { ...lines[0], costCenterId: "cc-1" },
            { ...lines[1], costCenterId: "cc-2" },
          ],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({ localVoucherCode: "JV-001" }),
    ]);
    expect(mock.inserts[1].insertRows()).toEqual([
      expect.objectContaining({ costCenterId: "cc-1" }),
      expect.objectContaining({ costCenterId: "cc-2" }),
    ]);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { header, lines: [{ ...lines[0], accountId: "missing-account" }, lines[1]] },
        commandCtx,
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { header, lines: [{ ...lines[0], accountId: inactiveAccount.id }, lines[1]] },
        commandCtx,
      ),
    );

    expectErr(result, AccountInactiveError);
  });

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

    const result = await mock.withTx((trx) =>
      run(trx, { header, lines: [{ ...lines[0], creditAmount: "1000" }, lines[1]] }, commandCtx),
    );

    expectErr(result, InvalidDebitCreditError);
  });
});
