import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  NoActiveChartOfAccountsError,
  AccountingPeriodNotFoundError,
  InvalidVariantError,
  TrialBalanceImbalancedError,
} from "../lib/errors.generated";
import {
  baseCompany,
  baseChartOfAccounts,
  baseAccountingPeriod,
  baseAccount,
  baseAccount2,
  baseAccountGroup,
  baseTrialBalance,
  baseTrialBalanceLine,
  revenueAccount,
  expenseAccount,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run, type GenerateTrialBalanceInput } from "./generateTrialBalance";

const baseInput: GenerateTrialBalanceInput = {
  companyId: baseCompany.id,
  accountingPeriodId: baseAccountingPeriod.id,
  variant: "PRE_CLOSE",
};

// Fixtures with accountGroupId for testing issue #1
const accountWithGroup = {
  ...baseAccount,
  accountGroupId: baseAccountGroup.id,
};

const account2WithGroup = {
  ...baseAccount2,
  accountGroupId: null,
};

describe("generateTrialBalance", () => {
  it("returns error when company has no active CoA", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // ChartOfAccounts lookup

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

    expectErr(result, NoActiveChartOfAccountsError);
  });

  it("returns error when accounting period not found", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(undefined); // AccountingPeriod lookup

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

    expectErr(result, AccountingPeriodNotFoundError);
  });

  it("returns error when variant is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod); // AccountingPeriod lookup

    const result = await run(db, { ...baseInput, variant: "INVALID" }, commandCtx);

    expectErr(result, InvalidVariantError);
  });

  it("generates trial balance with correct balances for PRE_CLOSE", async () => {
    const { db, spies } = createMockDb<Transaction>();

    const journalEntry = {
      id: "je-1",
      accountingPeriodId: baseAccountingPeriod.id,
      status: "POSTED",
      journalType: "MISCELLANEOUS",
    };

    const journalLine1 = {
      id: "jl-1",
      journalEntryId: "je-1",
      accountId: baseAccount.id,
      functionalDebitAmount: 5000,
      functionalCreditAmount: null,
    };

    const journalLine2 = {
      id: "jl-2",
      journalEntryId: "je-1",
      accountId: baseAccount2.id,
      functionalDebitAmount: null,
      functionalCreditAmount: 5000,
    };

    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-new",
      totalDebits: 5000,
      totalCredits: 5000,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount, baseAccount2]) // accounts
      .mockImplementationOnce(() => [journalEntry]) // journal entries
      .mockImplementationOnce(() => [journalLine1, journalLine2]) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup (no prior TB for upsert)

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance) // TrialBalance insert
      .mockReturnValueOnce({ ...baseTrialBalanceLine, trialBalanceId: "tb-new" }) // line 1
      .mockReturnValueOnce({
        ...baseTrialBalanceLine,
        trialBalanceId: "tb-new",
        accountId: baseAccount2.id,
      }); // line 2

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

    const value = expectOk(result);
    expect(value.trialBalance.totalDebits).toBe(5000);
    expect(value.trialBalance.totalCredits).toBe(5000);
    expect(value.trialBalance.id).toBe("tb-new");
  });

  it("includes zero-balance accounts from active CoA", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // No journal entries at all — all accounts should still appear
    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-zero",
      totalDebits: 0,
      totalCredits: 0,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount, baseAccount2]) // accounts (both active)
      .mockImplementationOnce(() => []) // journal entries (none)
      .mockImplementationOnce(() => []) // journal lines (none)
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup (no prior TB for upsert)

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance) // TrialBalance insert
      .mockReturnValueOnce({}) // line 1 insert
      .mockReturnValueOnce({}); // line 2 insert

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

    const value = expectOk(result);
    expect(value.trialBalance.totalDebits).toBe(0);
    expect(value.trialBalance.totalCredits).toBe(0);
    // Verify insert was called: 1 for TB + 2 for TB lines (one per account)
    expect(spies.insert).toHaveBeenCalledTimes(3);
  });

  it("returns error when trial balance is imbalanced", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // Create journal lines where debits != credits (imbalanced data)
    const journalEntry = {
      id: "je-1",
      accountingPeriodId: baseAccountingPeriod.id,
      status: "POSTED",
      journalType: "MISCELLANEOUS",
    };

    const journalLine1 = {
      id: "jl-1",
      journalEntryId: "je-1",
      accountId: baseAccount.id,
      functionalDebitAmount: 5000,
      functionalCreditAmount: null,
    };

    const journalLine2 = {
      id: "jl-2",
      journalEntryId: "je-1",
      accountId: baseAccount2.id,
      functionalDebitAmount: null,
      functionalCreditAmount: 3000, // intentionally imbalanced
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount, baseAccount2]) // accounts
      .mockImplementationOnce(() => [journalEntry]) // journal entries
      .mockImplementationOnce(() => [journalLine1, journalLine2]) // journal lines
      .mockReturnValueOnce(undefined); // preceding period lookup (no preceding period)

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

    expectErr(result, TrialBalanceImbalancedError);
  });

  it("emits audit event with correct actor and trial balance details", async () => {
    const { db, spies } = createMockDb<Transaction>();

    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-audit",
      totalDebits: 0,
      totalCredits: 0,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount, baseAccount2]) // accounts
      .mockImplementationOnce(() => []) // journal entries
      .mockImplementationOnce(() => []) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup (no prior TB for upsert)

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance)
      .mockReturnValueOnce({})
      .mockReturnValueOnce({});

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

    const value = expectOk(result);
    expect(value.auditEvent).toBeDefined();
    expect(value.auditEvent.type).toBe("TRIAL_BALANCE_GENERATED");
    expect(value.auditEvent.actorId).toBe(commandCtx.actorId);
    expect(value.auditEvent.timestamp).toBeInstanceOf(Date);
    expect(value.auditEvent.trialBalanceId).toBe("tb-audit");
    expect(value.auditEvent.variant).toBe("PRE_CLOSE");
    expect(value.auditEvent.isBalanced).toBe(true);
  });

  it("uses accountGroupId from Account record when available", async () => {
    const { db, spies } = createMockDb<Transaction>();

    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-group",
      totalDebits: 0,
      totalCredits: 0,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [accountWithGroup, account2WithGroup]) // accounts with groupId
      .mockImplementationOnce(() => []) // journal entries
      .mockImplementationOnce(() => []) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance)
      .mockReturnValueOnce({})
      .mockReturnValueOnce({});

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

    expectOk(result);

    // The first line insert should pass accountGroupId from the account fixture
    const firstLineValues = spies.values.mock.calls.find(
      (call: unknown[]) =>
        call[0] &&
        typeof call[0] === "object" &&
        "accountGroupId" in (call[0] as Record<string, unknown>) &&
        (call[0] as Record<string, unknown>).accountGroupId === baseAccountGroup.id,
    );
    expect(firstLineValues).toBeDefined();
  });

  it("zeros closing balances for revenue and expense accounts in POST_CLOSE variant", async () => {
    const { db, spies } = createMockDb<Transaction>();

    const postCloseInput: GenerateTrialBalanceInput = {
      ...baseInput,
      variant: "POST_CLOSE",
    };

    // Revenue account: 3000 debit, 8000 credit → closingBalance would be -5000 but POST_CLOSE zeros it
    // Expense account: 4000 debit, 4000 credit → balanced, still zeroed for POST_CLOSE
    // Asset account: 8000 debit, 3000 credit → not zeroed
    const journalEntry = {
      id: "je-post",
      accountingPeriodId: baseAccountingPeriod.id,
      status: "POSTED",
      journalType: "CLOSING",
    };

    const revenueJournalLine = {
      id: "jl-rev",
      journalEntryId: "je-post",
      accountId: revenueAccount.id,
      functionalDebitAmount: 3000,
      functionalCreditAmount: null,
    };

    const expenseJournalLine = {
      id: "jl-exp",
      journalEntryId: "je-post",
      accountId: expenseAccount.id,
      functionalDebitAmount: null,
      functionalCreditAmount: 3000,
    };

    // Total debits = 3000, total credits = 3000 — balanced
    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-post-close",
      variant: "POST_CLOSE" as const,
      totalDebits: 3000,
      totalCredits: 3000,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [revenueAccount, expenseAccount]) // accounts
      .mockImplementationOnce(() => [journalEntry]) // journal entries (CLOSING included for POST_CLOSE)
      .mockImplementationOnce(() => [revenueJournalLine, expenseJournalLine]) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance)
      .mockReturnValueOnce({})
      .mockReturnValueOnce({});

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

    expectOk(result);

    // Verify that TrialBalanceLine inserts have closingBalance = 0 for REVENUE/EXPENSE accounts
    const lineInsertCalls = spies.values.mock.calls.filter(
      (call: unknown[]) =>
        call[0] &&
        typeof call[0] === "object" &&
        "closingBalance" in (call[0] as Record<string, unknown>),
    );
    expect(lineInsertCalls.length).toBe(2);
    for (const call of lineInsertCalls) {
      expect((call[0] as Record<string, unknown>).closingBalance).toBe(0);
    }
  });

  it("deletes existing TrialBalance before inserting when regenerating for same period and variant", async () => {
    const { db, spies } = createMockDb<Transaction>();

    const existingTb = {
      ...baseTrialBalance,
      id: "tb-existing",
    };

    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-regenerated",
      totalDebits: 0,
      totalCredits: 0,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount, baseAccount2]) // accounts
      .mockImplementationOnce(() => []) // journal entries
      .mockImplementationOnce(() => []) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(existingTb); // existingTrialBalance lookup — found!

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance)
      .mockReturnValueOnce({})
      .mockReturnValueOnce({});

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

    expectOk(result);

    // delete spy should have been called twice: once for TrialBalanceLine, once for TrialBalance
    expect(spies.delete).toHaveBeenCalledTimes(2);
    // New trial balance should be inserted fresh
    expect(spies.insert).toHaveBeenCalledTimes(3); // 1 TB + 2 lines
  });

  it("sorts trial balance lines by account code ascending", async () => {
    const { db, spies } = createMockDb<Transaction>();

    // Provide accounts out of order — code "2000" before "1000"
    const insertedTrialBalance = {
      ...baseTrialBalance,
      id: "tb-sorted",
      totalDebits: 0,
      totalCredits: 0,
    };

    spies.select
      .mockReturnValueOnce(baseChartOfAccounts) // ChartOfAccounts lookup
      .mockReturnValueOnce(baseAccountingPeriod) // AccountingPeriod lookup
      .mockImplementationOnce(() => [baseAccount2, baseAccount]) // accounts: "2000" first, then "1000"
      .mockImplementationOnce(() => []) // journal entries
      .mockImplementationOnce(() => []) // journal lines
      .mockReturnValueOnce(undefined) // preceding period lookup (no preceding period)
      .mockReturnValueOnce(undefined); // existingTrialBalance lookup

    spies.insert
      .mockReturnValueOnce(insertedTrialBalance)
      .mockReturnValueOnce({})
      .mockReturnValueOnce({});

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

    expectOk(result);

    // The first TrialBalanceLine insert should be for account code "1000" (sortOrder: 1)
    // and second for "2000" (sortOrder: 2)
    const lineInsertCalls = spies.values.mock.calls.filter(
      (call: unknown[]) =>
        call[0] &&
        typeof call[0] === "object" &&
        "sortOrder" in (call[0] as Record<string, unknown>),
    );

    expect(lineInsertCalls.length).toBe(2);
    expect((lineInsertCalls[0][0] as Record<string, unknown>).accountCode).toBe("1000");
    expect((lineInsertCalls[0][0] as Record<string, unknown>).sortOrder).toBe(1);
    expect((lineInsertCalls[1][0] as Record<string, unknown>).accountCode).toBe("2000");
    expect((lineInsertCalls[1][0] as Record<string, unknown>).sortOrder).toBe(2);
  });
});
