import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountingPeriodNotFoundError,
  NotFinalPeriodError,
  StandardCloseNotCompleteError,
  DuplicateYearEndCloseError,
  RetainedEarningsAccountNotFoundError,
  NewFiscalYearNotDefinedError,
} from "../lib/errors.generated";
import {
  baseAccountingPeriod,
  lastPeriodOfYear,
  baseFiscalYear,
  baseFiscalYear2,
  softLockedPeriodClose,
  baseCompany,
  baseChartOfAccounts,
  retainedEarningsAccount,
  revenueAccount,
  expenseAccount,
  baseAccount,
  baseAccount2,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./executeYearEndClose";

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

    const result = await run(db, { accountingPeriodId: "non-existent" }, commandCtx);

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns error when period is not the final period of its fiscal year", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseAccountingPeriod) // period lookup (Jan, not final)
      .mockReturnValueOnce(baseFiscalYear); // fiscal year lookup

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

    expectErr(result, NotFinalPeriodError);
  });

  it("returns error when standard close checklist is not complete", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(lastPeriodOfYear) // period lookup (Dec, final)
      .mockReturnValueOnce(baseFiscalYear) // fiscal year lookup
      .mockReturnValueOnce(undefined); // no PeriodClose record

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

    expectErr(result, StandardCloseNotCompleteError);
  });

  it("returns error when year-end close has already been executed", async () => {
    const alreadyClosedFiscalYear = {
      ...baseFiscalYear,
      yearEndCloseExecuted: true,
    };
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(lastPeriodOfYear) // period lookup
      .mockReturnValueOnce(alreadyClosedFiscalYear) // fiscal year lookup
      .mockReturnValueOnce(softLockedPeriodClose); // period close lookup

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

    expectErr(result, DuplicateYearEndCloseError);
  });

  it("returns error when no retained earnings account exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(lastPeriodOfYear) // period lookup
      .mockReturnValueOnce(baseFiscalYear) // fiscal year lookup
      .mockReturnValueOnce(softLockedPeriodClose) // period close lookup
      .mockReturnValueOnce(baseChartOfAccounts) // chart of accounts lookup
      .mockReturnValueOnce(undefined); // no retained earnings account

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

    expectErr(result, RetainedEarningsAccountNotFoundError);
  });

  it("returns error when no new fiscal year is defined", async () => {
    const revenueJournalLine = {
      id: "jl-revenue",
      journalEntryId: "je-1",
      accountId: revenueAccount.id,
      classification: "REVENUE",
      debitAmount: null,
      creditAmount: 5000,
      functionalDebitAmount: null,
      functionalCreditAmount: 5000,
    };
    const expenseJournalLine = {
      id: "jl-expense",
      journalEntryId: "je-1",
      accountId: expenseAccount.id,
      classification: "EXPENSE",
      debitAmount: 3000,
      creditAmount: null,
      functionalDebitAmount: 3000,
      functionalCreditAmount: null,
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(lastPeriodOfYear) // period lookup
      .mockReturnValueOnce(baseFiscalYear) // fiscal year lookup
      .mockReturnValueOnce(softLockedPeriodClose) // period close lookup
      .mockReturnValueOnce(baseChartOfAccounts) // chart of accounts lookup
      .mockReturnValueOnce(retainedEarningsAccount) // retained earnings account
      .mockImplementationOnce(() => [{ id: lastPeriodOfYear.id }]) // allPeriods lookup
      .mockImplementationOnce(() => [revenueJournalLine, expenseJournalLine]); // journal lines with join
    spies.insert.mockReturnValueOnce({ id: "closing-entry-1" }); // closing entry insert
    spies.insert.mockImplementationOnce(() => []); // closing lines insert
    spies.select.mockReturnValueOnce(undefined); // no next fiscal year

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

    expectErr(result, NewFiscalYearNotDefinedError);
  });

  it("successfully executes year-end close", async () => {
    const revenueJournalLine = {
      id: "jl-revenue",
      journalEntryId: "je-1",
      accountId: revenueAccount.id,
      classification: "REVENUE",
      debitAmount: null,
      creditAmount: 5000,
      functionalDebitAmount: null,
      functionalCreditAmount: 5000,
    };
    const expenseJournalLine = {
      id: "jl-expense",
      journalEntryId: "je-1",
      accountId: expenseAccount.id,
      classification: "EXPENSE",
      debitAmount: 3000,
      creditAmount: null,
      functionalDebitAmount: 3000,
      functionalCreditAmount: null,
    };
    const assetJournalLine = {
      id: "jl-asset",
      journalEntryId: "je-1",
      accountId: baseAccount.id,
      classification: "ASSET",
      debitAmount: 10000,
      creditAmount: null,
      functionalDebitAmount: 10000,
      functionalCreditAmount: null,
    };
    const liabilityJournalLine = {
      id: "jl-liability",
      journalEntryId: "je-1",
      accountId: baseAccount2.id,
      classification: "LIABILITY",
      debitAmount: null,
      creditAmount: 8000,
      functionalDebitAmount: null,
      functionalCreditAmount: 8000,
    };

    const closingEntry = {
      id: "closing-entry-1",
      companyId: baseCompany.id,
      journalType: "CLOSING",
      status: "POSTED",
    };
    const openingEntry = {
      id: "opening-entry-1",
      companyId: baseCompany.id,
      journalType: "OPENING",
      status: "POSTED",
    };
    const newFirstPeriod = {
      id: "new-period-1",
      fiscalYearId: baseFiscalYear2.id,
      startDate: new Date("2025-01-01"),
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(lastPeriodOfYear) // period lookup
      .mockReturnValueOnce(baseFiscalYear) // fiscal year lookup
      .mockReturnValueOnce(softLockedPeriodClose) // period close lookup
      .mockReturnValueOnce(baseChartOfAccounts) // chart of accounts lookup
      .mockReturnValueOnce(retainedEarningsAccount) // retained earnings account
      .mockImplementationOnce(() => [{ id: lastPeriodOfYear.id }]) // allPeriods lookup
      .mockImplementationOnce(() => [
        revenueJournalLine,
        expenseJournalLine,
        assetJournalLine,
        liabilityJournalLine,
      ]); // journal lines with join

    // After closing entry creation
    spies.insert.mockReturnValueOnce(closingEntry); // closing entry insert
    spies.insert.mockImplementationOnce(() => []); // closing lines insert

    // Next fiscal year lookup
    spies.select.mockReturnValueOnce(baseFiscalYear2);
    // New first period lookup
    spies.select.mockReturnValueOnce(newFirstPeriod);

    // Opening entry creation
    spies.insert.mockReturnValueOnce(openingEntry); // opening entry insert
    spies.insert.mockImplementationOnce(() => []); // opening lines insert

    // Fiscal year update and period close update
    spies.update.mockReturnValueOnce({ ...baseFiscalYear, yearEndCloseExecuted: true });
    spies.update.mockReturnValueOnce({ ...softLockedPeriodClose, yearEndCloseExecuted: true });

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

    const value = expectOk(result);
    expect(value.closingEntry).toBeDefined();
    expect(value.closingEntry.journalType).toBe("CLOSING");
    expect(value.closingEntry.status).toBe("POSTED");
    expect(value.openingEntry).toBeDefined();
    expect(value.openingEntry.journalType).toBe("OPENING");
    expect(value.openingEntry.status).toBe("POSTED");
    expect(value.auditTrail).toBeDefined();
    expect(value.auditTrail.length).toBeGreaterThan(0);
    expect(value.auditTrail[0].type).toBe("YEAR_END_CLOSE");
    expect(value.auditTrail[0].netIncome).toBe(2000); // 5000 revenue - 3000 expense
    expect(spies.update).toHaveBeenCalled();
  });
});
