import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  AccountingPeriodNotFoundError,
  InvalidJournalTypeError,
  DuplicateReferenceNumberError,
  InvalidEntryDateError,
} from "../lib/errors.generated";
import { baseCompany, baseAccountingPeriod, baseJournalEntry } from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./createJournalEntry";

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

    const result = await run(
      db,
      {
        companyId: "missing-company",
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      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,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectErr(result, CompanyNotFoundError);
  });

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

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: "missing-period",
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectErr(result, AccountingPeriodNotFoundError);
  });

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

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "INVALID_TYPE",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectErr(result, InvalidJournalTypeError);
  });

  it("returns error when reference number already exists within the company", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(baseJournalEntry); // existing entry with same ref

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: baseJournalEntry.referenceNumber,
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectErr(result, DuplicateReferenceNumberError);
  });

  it("returns error when entry date is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined); // no duplicate ref

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-NEW",
        entryDate: new Date("invalid-date"),
      },
      commandCtx,
    );

    expectErr(result, InvalidEntryDateError);
  });

  it("creates journal entry in DRAFT status with required fields", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined); // no duplicate ref
    spies.insert.mockReturnValueOnce(baseJournalEntry);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

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

  it("creates journal entry with optional description", async () => {
    const entryWithDesc = { ...baseJournalEntry, description: "Monthly closing" };
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(entryWithDesc);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
        description: "Monthly closing",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalEntry.description).toBe("Monthly closing");
  });

  it("creates journal entry with optional source document reference", async () => {
    const entryWithSrc = { ...baseJournalEntry, sourceDocumentReference: "INV-001" };
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(entryWithSrc);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
        sourceDocumentReference: "INV-001",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalEntry.sourceDocumentReference).toBe("INV-001");
  });

  it("journal entries from different companies are isolated", async () => {
    const companyB = { id: "company-2", status: "ACTIVE" };
    const entryCompanyB = { ...baseJournalEntry, id: "je-company-b", companyId: companyB.id };
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(companyB)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(entryCompanyB);

    const result = await run(
      db,
      {
        companyId: companyB.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalEntry.companyId).toBe(companyB.id);
  });

  it("emits audit event recording acting user, timestamp, and initial field values", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseCompany)
      .mockReturnValueOnce(baseAccountingPeriod)
      .mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(baseJournalEntry);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        accountingPeriodId: baseAccountingPeriod.id,
        journalType: "MISCELLANEOUS",
        referenceNumber: "JE-001",
        entryDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectOk(result);
    expect(spies.insert).toHaveBeenCalled();
  });
});
