import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PeriodCloseNotFoundError,
  InvalidStatusTransitionError,
  InvalidAdjustingEntryTypeError,
  UnbalancedAdjustingEntriesError,
} from "../lib/errors.generated";
import {
  subledgersVerifiedPeriodClose,
  inProgressPeriodClose,
  adjustmentsPostedPeriodClose,
  baseAccount,
  baseAccount2,
  baseAccountingPeriod,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run, type PostAdjustingEntriesInput } from "./postAdjustingEntries";

const balancedAccrualInput: PostAdjustingEntriesInput = {
  periodCloseId: subledgersVerifiedPeriodClose.id,
  adjustingEntries: [
    {
      adjustingEntryType: "ACCRUAL",
      description: "Accrued revenue",
      lines: [
        { accountId: baseAccount.id, debitAmount: 1000, creditAmount: null, description: "Debit" },
        {
          accountId: baseAccount2.id,
          debitAmount: null,
          creditAmount: 1000,
          description: "Credit",
        },
      ],
    },
  ],
};

const balancedDeferralInput: PostAdjustingEntriesInput = {
  periodCloseId: subledgersVerifiedPeriodClose.id,
  adjustingEntries: [
    {
      adjustingEntryType: "DEFERRAL",
      description: "Deferred expense",
      lines: [
        { accountId: baseAccount.id, debitAmount: 500, creditAmount: null, description: "Debit" },
        {
          accountId: baseAccount2.id,
          debitAmount: null,
          creditAmount: 500,
          description: "Credit",
        },
      ],
    },
  ],
};

const balancedReclassificationInput: PostAdjustingEntriesInput = {
  periodCloseId: subledgersVerifiedPeriodClose.id,
  adjustingEntries: [
    {
      adjustingEntryType: "RECLASSIFICATION",
      description: "Reclassification entry",
      lines: [
        { accountId: baseAccount.id, debitAmount: 2000, creditAmount: null, description: "Debit" },
        {
          accountId: baseAccount2.id,
          debitAmount: null,
          creditAmount: 2000,
          description: "Credit",
        },
      ],
    },
  ],
};

describe("postAdjustingEntries", () => {
  it("returns error when PeriodClose record does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // PeriodClose lookup

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

    expectErr(result, PeriodCloseNotFoundError);
  });

  it("returns error when PeriodClose is in IN_PROGRESS status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(inProgressPeriodClose);

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

    expectErr(result, InvalidStatusTransitionError);
  });

  it("returns error when PeriodClose is already in ADJUSTMENTS_POSTED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(adjustmentsPostedPeriodClose);

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

    expectErr(result, InvalidStatusTransitionError);
  });

  it("returns error when adjusting entry type is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(subledgersVerifiedPeriodClose);

    const input: PostAdjustingEntriesInput = {
      periodCloseId: subledgersVerifiedPeriodClose.id,
      adjustingEntries: [
        {
          adjustingEntryType: "INVALID_TYPE",
          description: "Bad type",
          lines: [
            {
              accountId: baseAccount.id,
              debitAmount: 100,
              creditAmount: null,
              description: "Debit",
            },
            {
              accountId: baseAccount2.id,
              debitAmount: null,
              creditAmount: 100,
              description: "Credit",
            },
          ],
        },
      ],
    };

    const result = await run(db, input, commandCtx);

    expectErr(result, InvalidAdjustingEntryTypeError);
  });

  it("returns error when adjusting entries are not balanced", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(subledgersVerifiedPeriodClose);

    const input: PostAdjustingEntriesInput = {
      periodCloseId: subledgersVerifiedPeriodClose.id,
      adjustingEntries: [
        {
          adjustingEntryType: "ACCRUAL",
          description: "Unbalanced entry",
          lines: [
            {
              accountId: baseAccount.id,
              debitAmount: 1000,
              creditAmount: null,
              description: "Debit",
            },
            {
              accountId: baseAccount2.id,
              debitAmount: null,
              creditAmount: 500,
              description: "Credit",
            },
          ],
        },
      ],
    };

    const result = await run(db, input, commandCtx);

    expectErr(result, UnbalancedAdjustingEntriesError);
  });

  it("posts accrual adjusting entries and transitions to ADJUSTMENTS_POSTED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-accrual-1",
      adjustingEntryType: "ACCRUAL",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedAccrualInput, commandCtx);

    const value = expectOk(result);
    expect(value.periodClose.status).toBe("ADJUSTMENTS_POSTED");
    expect(value.postedEntries).toHaveLength(1);
    expect(value.postedEntries[0].adjustingEntryType).toBe("ACCRUAL");
  });

  it("posts deferral adjusting entries and transitions to ADJUSTMENTS_POSTED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-deferral-1",
      adjustingEntryType: "DEFERRAL",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedDeferralInput, commandCtx);

    const value = expectOk(result);
    expect(value.periodClose.status).toBe("ADJUSTMENTS_POSTED");
    expect(value.postedEntries).toHaveLength(1);
    expect(value.postedEntries[0].adjustingEntryType).toBe("DEFERRAL");
  });

  it("posts reclassification adjusting entries and transitions to ADJUSTMENTS_POSTED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-reclass-1",
      adjustingEntryType: "RECLASSIFICATION",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedReclassificationInput, commandCtx);

    const value = expectOk(result);
    expect(value.periodClose.status).toBe("ADJUSTMENTS_POSTED");
    expect(value.postedEntries).toHaveLength(1);
    expect(value.postedEntries[0].adjustingEntryType).toBe("RECLASSIFICATION");
  });

  it("adjusting entries are flagged distinctly from regular journal entries", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-accrual-1",
      adjustingEntryType: "ACCRUAL",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedAccrualInput, commandCtx);

    const value = expectOk(result);
    // Adjusting entries have adjustingEntryType set (not null), unlike regular journal entries
    for (const entry of value.postedEntries) {
      expect(entry.adjustingEntryType).not.toBeNull();
      expect(VALID_ADJUSTING_ENTRY_TYPES).toContain(entry.adjustingEntryType);
    }
  });

  it("updates completionPercentage after posting adjusting entries", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-accrual-1",
      adjustingEntryType: "ACCRUAL",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedAccrualInput, commandCtx);

    const value = expectOk(result);
    expect(value.periodClose.completionPercentage).toBe(50);
    expect(value.periodClose.completionPercentage).toBeGreaterThan(
      subledgersVerifiedPeriodClose.completionPercentage,
    );
  });

  it("emits audit event recording adjusting entry details and status transition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(subledgersVerifiedPeriodClose)
      .mockReturnValueOnce(baseAccountingPeriod);

    const postedJournalEntry = {
      id: "je-accrual-1",
      adjustingEntryType: "ACCRUAL",
      status: "POSTED",
    };
    const postedLine1 = { id: "line-1" };
    const postedLine2 = { id: "line-2" };
    spies.insert
      .mockReturnValueOnce(postedJournalEntry)
      .mockReturnValueOnce(postedLine1)
      .mockReturnValueOnce(postedLine2);

    const updatedPeriodClose = {
      ...subledgersVerifiedPeriodClose,
      status: "ADJUSTMENTS_POSTED",
      completionPercentage: 50,
    };
    spies.update.mockReturnValueOnce(updatedPeriodClose);

    const result = await run(db, balancedAccrualInput, commandCtx);

    const value = expectOk(result);
    expect(value.auditEvent).toBeDefined();
    expect(value.auditEvent.type).toBe("ADJUSTING_ENTRIES_POSTED");
    expect(value.auditEvent.actorId).toBe(commandCtx.actorId);
    expect(value.auditEvent.from).toBe("SUBLEDGERS_VERIFIED");
    expect(value.auditEvent.to).toBe("ADJUSTMENTS_POSTED");
    expect(value.auditEvent.timestamp).toBeDefined();
    expect(value.auditEvent.adjustingEntries).toBeDefined();
    expect(value.auditEvent.adjustingEntries).toHaveLength(1);
    expect(value.auditEvent.adjustingEntries[0].adjustingEntryType).toBe("ACCRUAL");
  });
});

const VALID_ADJUSTING_ENTRY_TYPES = ["ACCRUAL", "DEFERRAL", "RECLASSIFICATION"];
