import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForPostingError,
  MinimumLinesNotMetError,
  UnbalancedEntryError,
  InvalidPeriodStatusError,
  InactiveAccountError,
  SuccessorAccountInvalidError,
} from "../lib/errors.generated";
import {
  baseJournalEntry,
  postedJournalEntry,
  reversedJournalEntry,
  baseJournalLine,
  baseJournalLine2,
  baseAccountingPeriod,
  futureEnterablePeriod,
  neverOpenedPeriod,
  closedPeriod,
  permanentlyClosedPeriod,
  baseAccount,
  baseAccount2,
  inactiveAccount,
  deprecatedAccount,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./postJournalEntry";

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

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

    expectErr(result, JournalEntryNotFoundError);
  });

  it("returns error when journal entry is already POSTED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(postedJournalEntry);

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

    expectErr(result, InvalidStatusForPostingError);
  });

  it("returns error when journal entry is REVERSED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(reversedJournalEntry);

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

    expectErr(result, InvalidStatusForPostingError);
  });

  it("returns error when entry has fewer than two journal lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockReturnValueOnce(undefined); // lines query returns via execute()
    // execute() for lines returns an array with only one line
    spies.select.mockReset();
    spies.select
      .mockReturnValueOnce(baseJournalEntry)
      .mockImplementationOnce(() => [baseJournalLine]); // journal lines

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

    expectErr(result, MinimumLinesNotMetError);
  });

  it("returns error when entry has only one line (debit only or credit only)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const singleLine = { ...baseJournalLine };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [singleLine]); // only one journal line

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

    expectErr(result, MinimumLinesNotMetError);
  });

  it("returns error when total debits do not equal total credits in functional currency", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unbalancedCreditLine = {
      ...baseJournalLine2,
      functionalCreditAmount: 500, // doesn't match debit of 1000
    };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, unbalancedCreditLine]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod); // period lookup

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

    expectErr(result, UnbalancedEntryError);
  });

  it("returns error when target period is in NEVER_OPENED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const entryInNeverOpened = {
      ...baseJournalEntry,
      accountingPeriodId: neverOpenedPeriod.id,
    };
    spies.select
      .mockReturnValueOnce(entryInNeverOpened) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(neverOpenedPeriod); // period lookup

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when target period is in CLOSED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const entryInClosed = {
      ...baseJournalEntry,
      accountingPeriodId: closedPeriod.id,
    };
    spies.select
      .mockReturnValueOnce(entryInClosed) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(closedPeriod); // period lookup

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when target period is in PERMANENTLY_CLOSED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const entryInPermClosed = {
      ...baseJournalEntry,
      accountingPeriodId: permanentlyClosedPeriod.id,
    };
    spies.select
      .mockReturnValueOnce(entryInPermClosed) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(permanentlyClosedPeriod); // period lookup

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

    expectErr(result, InvalidPeriodStatusError);
  });

  it("returns error when a GL account is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const lineWithInactiveAccount = {
      ...baseJournalLine,
      accountId: inactiveAccount.id,
    };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [lineWithInactiveAccount, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(inactiveAccount) // account lookup for inactive account
      .mockReturnValueOnce(baseAccount2); // account lookup for active account

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

    expectErr(result, InactiveAccountError);
  });

  it("returns error when a GL account is in draft status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const draftAccount = { ...baseAccount, id: "account-draft", status: "DRAFT" };
    const lineWithDraftAccount = {
      ...baseJournalLine,
      accountId: draftAccount.id,
    };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [lineWithDraftAccount, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(draftAccount) // account lookup for draft account
      .mockReturnValueOnce(baseAccount2); // account lookup for active account

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

    expectErr(result, InactiveAccountError);
  });

  it("returns error when successor account is itself inactive without further successor", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deprecatedWithInactiveSuccessor = {
      ...deprecatedAccount,
      successorAccountId: inactiveAccount.id,
    };
    const inactiveSuccessor = { ...inactiveAccount, successorAccountId: null };
    const lineWithDeprecatedAccount = {
      ...baseJournalLine,
      accountId: deprecatedWithInactiveSuccessor.id,
    };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [lineWithDeprecatedAccount, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(deprecatedWithInactiveSuccessor) // deprecated account lookup
      .mockReturnValueOnce(inactiveSuccessor) // successor account lookup (inactive)
      .mockReturnValueOnce(baseAccount2); // second line account lookup

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

    expectErr(result, SuccessorAccountInvalidError);
  });

  it("posts balanced DRAFT entry to OPEN period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup (OPEN)
      .mockReturnValueOnce(baseAccount) // account lookup for line 1
      .mockReturnValueOnce(baseAccount2); // account lookup for line 2
    spies.update.mockReturnValueOnce(postedEntry);

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

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

  it("posts balanced DRAFT entry to FUTURE_ENTERABLE period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const entryInFuture = {
      ...baseJournalEntry,
      accountingPeriodId: futureEnterablePeriod.id,
    };
    const postedEntry = { ...entryInFuture, status: "POSTED", postedAt: new Date() };
    spies.select
      .mockReturnValueOnce(entryInFuture) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(futureEnterablePeriod) // period lookup (FUTURE_ENTERABLE)
      .mockReturnValueOnce(baseAccount) // account lookup for line 1
      .mockReturnValueOnce(baseAccount2); // account lookup for line 2
    spies.update.mockReturnValueOnce(postedEntry);

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

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

  it("redirects deprecated GL account to successor account at posting time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const lineWithDeprecatedAccount = {
      ...baseJournalLine,
      accountId: deprecatedAccount.id,
    };
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    const updatedLine = { ...lineWithDeprecatedAccount, accountId: baseAccount.id };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [lineWithDeprecatedAccount, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(deprecatedAccount) // deprecated account lookup
      .mockReturnValueOnce(baseAccount) // successor account lookup (ACTIVE)
      .mockReturnValueOnce(baseAccount2); // second line account lookup
    spies.update
      .mockReturnValueOnce(updatedLine) // journal line update (redirect)
      .mockReturnValueOnce(postedEntry); // journal entry update

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

    const value = expectOk(result);
    expect(value.journalEntry.status).toBe("POSTED");
    expect(value.auditTrail).toBeDefined();
    expect(value.auditTrail.length).toBeGreaterThan(0);
    expect(value.auditTrail[0].type).toBe("SUCCESSOR_REDIRECT");
  });

  it("records successor account redirect in audit trail", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const lineWithDeprecatedAccount = {
      ...baseJournalLine,
      accountId: deprecatedAccount.id,
    };
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    const updatedLine = { ...lineWithDeprecatedAccount, accountId: baseAccount.id };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [lineWithDeprecatedAccount, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(deprecatedAccount) // deprecated account lookup
      .mockReturnValueOnce(baseAccount) // successor account lookup (ACTIVE)
      .mockReturnValueOnce(baseAccount2); // second line account lookup
    spies.update
      .mockReturnValueOnce(updatedLine) // journal line update (redirect)
      .mockReturnValueOnce(postedEntry); // journal entry update

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

    const value = expectOk(result);
    expect(value.auditTrail).toContainEqual(
      expect.objectContaining({
        type: "SUCCESSOR_REDIRECT",
        originalAccountId: deprecatedAccount.id,
        successorAccountId: baseAccount.id,
      }),
    );
  });

  it("entry becomes immutable after posting", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(baseAccount) // account lookup for line 1
      .mockReturnValueOnce(baseAccount2); // account lookup for line 2
    spies.update.mockReturnValueOnce(postedEntry);

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

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

  it("emits audit event recording status transition from DRAFT to POSTED and posting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedEntry = { ...baseJournalEntry, status: "POSTED", postedAt: new Date() };
    spies.select
      .mockReturnValueOnce(baseJournalEntry) // journal entry lookup
      .mockImplementationOnce(() => [baseJournalLine, baseJournalLine2]) // journal lines
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup
      .mockReturnValueOnce(baseAccount) // account lookup for line 1
      .mockReturnValueOnce(baseAccount2); // account lookup for line 2
    spies.update.mockReturnValueOnce(postedEntry);

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

    const value = expectOk(result);
    expect(value.journalEntry.status).toBe("POSTED");
    // Audit event is emitted as part of successful posting
    expect(value.auditTrail).toBeDefined();
  });
});
