import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  ChartOfAccountsNotFoundError,
  InvalidStatementTypeError,
  InvalidReportingParametersError,
  NoTrialBalanceDataError,
  ComparativeDataUnavailableError,
} from "../lib/errors.generated";
import {
  baseCompany,
  baseChartOfAccounts,
  baseTrialBalance,
  baseTrialBalanceLine,
  baseFinancialStatement,
  baseAccount,
  revenueAccount,
  expenseAccount,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run, type GenerateFinancialStatementInput } from "./generateFinancialStatement";

describe("generateFinancialStatement", () => {
  const balanceSheetInput: GenerateFinancialStatementInput = {
    companyId: baseCompany.id,
    chartOfAccountsId: baseChartOfAccounts.id,
    trialBalanceId: baseTrialBalance.id,
    statementType: "BALANCE_SHEET",
    reportingDate: new Date("2024-01-31"),
  };

  const profitAndLossInput: GenerateFinancialStatementInput = {
    companyId: baseCompany.id,
    chartOfAccountsId: baseChartOfAccounts.id,
    trialBalanceId: baseTrialBalance.id,
    statementType: "PROFIT_AND_LOSS",
    startDate: new Date("2024-01-01"),
    endDate: new Date("2024-01-31"),
  };

  it("returns error when company does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { ...balanceSheetInput, companyId: "missing" }, commandCtx);

    expectErr(result, CompanyNotFoundError);
  });

  it("returns error when company is not ACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...baseCompany, status: "INACTIVE" });

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

    expectErr(result, CompanyNotFoundError);
  });

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

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

    expectErr(result, ChartOfAccountsNotFoundError);
  });

  it("returns error when CoA is not ACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce({ ...baseChartOfAccounts, status: "INACTIVE" });

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

    expectErr(result, ChartOfAccountsNotFoundError);
  });

  it("returns error for invalid statement type", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(baseChartOfAccounts);

    const result = await run(
      db,
      { ...balanceSheetInput, statementType: "INVALID_TYPE" },
      commandCtx,
    );

    expectErr(result, InvalidStatementTypeError);
  });

  it("returns error when BALANCE_SHEET is missing reportingDate", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(baseChartOfAccounts);

    const input: GenerateFinancialStatementInput = {
      ...balanceSheetInput,
      reportingDate: undefined,
    };
    const result = await run(db, input, commandCtx);

    expectErr(result, InvalidReportingParametersError);
  });

  it("returns error when PROFIT_AND_LOSS is missing date range", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(baseChartOfAccounts);

    const input: GenerateFinancialStatementInput = {
      companyId: baseCompany.id,
      chartOfAccountsId: baseChartOfAccounts.id,
      trialBalanceId: baseTrialBalance.id,
      statementType: "PROFIT_AND_LOSS",
    };
    const result = await run(db, input, commandCtx);

    expectErr(result, InvalidReportingParametersError);
  });

  it("returns error when CASH_FLOW is missing cashFlowMethod", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseCompany).mockReturnValueOnce(baseChartOfAccounts);

    const input: GenerateFinancialStatementInput = {
      companyId: baseCompany.id,
      chartOfAccountsId: baseChartOfAccounts.id,
      trialBalanceId: baseTrialBalance.id,
      statementType: "CASH_FLOW",
      startDate: new Date("2024-01-01"),
      endDate: new Date("2024-01-31"),
    };
    const result = await run(db, input, commandCtx);

    expectErr(result, InvalidReportingParametersError);
  });

  it("returns error when trial balance does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(undefined); // trial balance

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

    expectErr(result, NoTrialBalanceDataError);
  });

  it("returns error when comparative trial balance does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance
      .mockReturnValueOnce(undefined); // comparative trial balance

    const result = await run(
      db,
      { ...balanceSheetInput, comparativePeriodTrialBalanceId: "missing-comparative" },
      commandCtx,
    );

    expectErr(result, ComparativeDataUnavailableError);
  });

  it("successfully generates a BALANCE_SHEET financial statement", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance
      .mockReturnValueOnce([baseTrialBalanceLine]) // trial balance lines (execute)
      .mockReturnValueOnce(baseAccount); // account lookup for line
    spies.insert
      .mockReturnValueOnce(baseFinancialStatement) // FinancialStatement insert
      .mockReturnValueOnce({
        ...baseTrialBalanceLine,
        id: "fs-line-1",
        financialStatementId: baseFinancialStatement.id,
      }) // DETAIL line item insert
      .mockReturnValueOnce({
        id: "fs-line-subtotal-1",
        financialStatementId: baseFinancialStatement.id,
        lineType: "SUBTOTAL",
      }); // SUBTOTAL line item insert

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

    const value = expectOk(result);
    expect(value.financialStatement).toEqual(baseFinancialStatement);
    expect(value.auditTrail).toBeDefined();
    expect(value.auditTrail.length).toBeGreaterThan(0);
  });

  it("successfully generates a PROFIT_AND_LOSS financial statement", async () => {
    const plTrialBalanceLine = {
      ...baseTrialBalanceLine,
      id: "tb-line-revenue",
      accountId: revenueAccount.id,
      accountCode: revenueAccount.code,
      accountName: revenueAccount.name,
      closingBalance: 8000,
    };
    const plExpenseLine = {
      ...baseTrialBalanceLine,
      id: "tb-line-expense",
      accountId: expenseAccount.id,
      accountCode: expenseAccount.code,
      accountName: expenseAccount.name,
      closingBalance: 3000,
    };
    const plFinancialStatement = {
      ...baseFinancialStatement,
      id: "financial-statement-pl",
      statementType: "PROFIT_AND_LOSS",
      reportingDate: null,
      startDate: new Date("2024-01-01"),
      endDate: new Date("2024-01-31"),
    };

    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance
      .mockReturnValueOnce([plTrialBalanceLine, plExpenseLine]) // trial balance lines
      .mockReturnValueOnce(revenueAccount) // account lookup for revenue line
      .mockReturnValueOnce(expenseAccount); // account lookup for expense line
    spies.insert
      .mockReturnValueOnce(plFinancialStatement) // FinancialStatement insert
      .mockReturnValueOnce({}) // DETAIL line item 1 (revenue)
      .mockReturnValueOnce({}) // DETAIL line item 2 (expense)
      .mockReturnValueOnce({}) // SUBTOTAL line item for Revenue
      .mockReturnValueOnce({}); // SUBTOTAL line item for Expenses

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

    const value = expectOk(result);
    expect(value.financialStatement.statementType).toBe("PROFIT_AND_LOSS");
  });

  it("returns error when trial balance has no lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance exists
      .mockReturnValueOnce([]); // but has no lines

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

    expectErr(result, NoTrialBalanceDataError);
  });

  it("emits audit event recording the generation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance
      .mockReturnValueOnce([baseTrialBalanceLine]) // trial balance lines
      .mockReturnValueOnce(baseAccount); // account lookup for line
    spies.insert
      .mockReturnValueOnce(baseFinancialStatement) // FinancialStatement insert
      .mockReturnValueOnce({}) // DETAIL line item
      .mockReturnValueOnce({}); // SUBTOTAL line item

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

    const value = expectOk(result);
    expect(value.auditTrail).toEqual([
      {
        type: "FINANCIAL_STATEMENT_GENERATED",
        statementType: "BALANCE_SHEET",
        actorId: commandCtx.actorId,
      },
    ]);
  });

  it("generates subtotal lines for each section", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany) // company
      .mockReturnValueOnce(baseChartOfAccounts) // CoA
      .mockReturnValueOnce(baseTrialBalance) // trial balance
      .mockReturnValueOnce([baseTrialBalanceLine]) // trial balance lines
      .mockReturnValueOnce(baseAccount); // account lookup for line
    const subtotalLineItem = {
      id: "fs-line-subtotal-1",
      financialStatementId: baseFinancialStatement.id,
      lineType: "SUBTOTAL",
      label: "Total Assets",
      amount: 3000,
      sectionType: "Assets",
    };
    spies.insert
      .mockReturnValueOnce(baseFinancialStatement) // FinancialStatement insert
      .mockReturnValueOnce({}) // DETAIL line item
      .mockReturnValueOnce(subtotalLineItem); // SUBTOTAL line item

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

    expectOk(result);
    // Verify insert was called for both DETAIL and SUBTOTAL lines (2 line item inserts)
    expect(spies.insert).toHaveBeenCalledTimes(3); // 1 statement + 2 line items
  });
});
