import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  FiscalYearNotFoundError,
  NameRequiredError,
  InvalidDateRangeError,
  OverlappingPeriodError,
  InvalidPeriodTypeError,
  DuplicatePeriodDatesError,
} from "../lib/errors.generated";
import {
  baseCompany,
  baseFiscalYear,
  baseAccountingPeriod,
  lastPeriodOfYear,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run, type CreateAccountingPeriodInput } from "./createAccountingPeriod";

const validInput: CreateAccountingPeriodInput = {
  companyId: baseCompany.id,
  fiscalYearId: baseFiscalYear.id,
  name: "January 2024",
  startDate: new Date("2024-01-01"),
  endDate: new Date("2024-01-31"),
  periodType: "OPERATING",
};

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

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

    expectErr(result, FiscalYearNotFoundError);
  });

  it("returns error when fiscal year belongs to a different company", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...baseFiscalYear, companyId: "other-company" });

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

    expectErr(result, FiscalYearNotFoundError);
  });

  it("returns error when name is empty", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear);

    const result = await run(db, { ...validInput, name: "" }, commandCtx);

    expectErr(result, NameRequiredError);
  });

  it("returns error when start date is not before end date", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear);

    const result = await run(
      db,
      {
        ...validInput,
        startDate: new Date("2024-01-31"),
        endDate: new Date("2024-01-01"),
      },
      commandCtx,
    );

    expectErr(result, InvalidDateRangeError);
  });

  it("returns error when start date equals end date", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear);

    const result = await run(
      db,
      {
        ...validInput,
        startDate: new Date("2024-01-15"),
        endDate: new Date("2024-01-15"),
      },
      commandCtx,
    );

    expectErr(result, InvalidDateRangeError);
  });

  it("returns error when period type is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([]);

    const result = await run(db, { ...validInput, periodType: "MONTHLY" }, commandCtx);

    expectErr(result, InvalidPeriodTypeError);
  });

  it("returns error when period overlaps with an existing period in the same fiscal year", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([baseAccountingPeriod]);

    const result = await run(
      db,
      {
        ...validInput,
        name: "Overlapping Period",
        startDate: new Date("2024-01-15"),
        endDate: new Date("2024-02-15"),
      },
      commandCtx,
    );

    expectErr(result, OverlappingPeriodError);
  });

  it("returns error when two periods have identical dates and neither is adjustment type", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([baseAccountingPeriod]);

    const result = await run(
      db,
      {
        ...validInput,
        name: "Duplicate Period",
        startDate: baseAccountingPeriod.startDate,
        endDate: baseAccountingPeriod.endDate,
        periodType: "OPERATING",
      },
      commandCtx,
    );

    expectErr(result, DuplicatePeriodDatesError);
  });

  it("creates period in NEVER_OPENED status with valid inputs", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const expectedPeriod = {
      ...baseAccountingPeriod,
      status: "NEVER_OPENED",
    };
    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([]);
    spies.insert.mockReturnValueOnce(expectedPeriod);

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

    const value = expectOk(result);
    expect(value.accountingPeriod.status).toBe("NEVER_OPENED");
  });

  it("creates 12 monthly periods with correct date boundaries for a calendar fiscal year", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const existingPeriods: unknown[] = [];
    const results: unknown[] = [];

    for (let month = 0; month < 12; month++) {
      const startDate = new Date(2024, month, 1);
      const endDate = new Date(2024, month + 1, 0);
      const periodName = startDate.toLocaleString("en", { month: "long", year: "numeric" });

      spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([...existingPeriods]);

      const createdPeriod = {
        id: `period-${month + 1}`,
        companyId: baseCompany.id,
        fiscalYearId: baseFiscalYear.id,
        name: periodName,
        startDate,
        endDate,
        periodType: "OPERATING",
        status: "NEVER_OPENED",
        createdAt: new Date(),
        updatedAt: null,
      };
      spies.insert.mockReturnValueOnce(createdPeriod);

      const result = await run(
        db,
        {
          companyId: baseCompany.id,
          fiscalYearId: baseFiscalYear.id,
          name: periodName,
          startDate,
          endDate,
          periodType: "OPERATING",
        },
        commandCtx,
      );

      const value = expectOk(result);
      results.push(value.accountingPeriod);
      existingPeriods.push(createdPeriod);
    }

    expect(results).toHaveLength(12);
  });

  it("creates 13 periods (12 operating + 1 adjustment) for a fiscal year", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const existingPeriods: unknown[] = [];

    // Create 12 operating periods
    for (let month = 0; month < 12; month++) {
      const startDate = new Date(2024, month, 1);
      const endDate = new Date(2024, month + 1, 0);

      const createdPeriod = {
        id: `period-${month + 1}`,
        companyId: baseCompany.id,
        fiscalYearId: baseFiscalYear.id,
        name: `Period ${month + 1}`,
        startDate,
        endDate,
        periodType: "OPERATING",
        status: "NEVER_OPENED",
        createdAt: new Date(),
        updatedAt: null,
      };

      spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([...existingPeriods]);
      spies.insert.mockReturnValueOnce(createdPeriod);

      await run(
        db,
        {
          companyId: baseCompany.id,
          fiscalYearId: baseFiscalYear.id,
          name: `Period ${month + 1}`,
          startDate,
          endDate,
          periodType: "OPERATING",
        },
        commandCtx,
      );

      existingPeriods.push(createdPeriod);
    }

    // Create 1 adjustment period sharing the last period's end date
    const adjStartDate = new Date(2024, 11, 1);
    const adjEndDate = new Date(2024, 12, 0); // Dec 31 — same as the loop-built December period

    const adjustmentPeriod = {
      id: "period-13",
      companyId: baseCompany.id,
      fiscalYearId: baseFiscalYear.id,
      name: "Year-End Adjustment",
      startDate: adjStartDate,
      endDate: adjEndDate,
      periodType: "ADJUSTMENT",
      status: "NEVER_OPENED",
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([...existingPeriods]);
    spies.insert.mockReturnValueOnce(adjustmentPeriod);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        fiscalYearId: baseFiscalYear.id,
        name: "Year-End Adjustment",
        startDate: adjStartDate,
        endDate: adjEndDate,
        periodType: "ADJUSTMENT",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.accountingPeriod.periodType).toBe("ADJUSTMENT");
    expect(spies.insert).toHaveBeenCalledTimes(13);
  });

  it("adjustment period shares the same end date as the final regular period", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const adjustmentResult = {
      id: "period-adjustment",
      companyId: baseCompany.id,
      fiscalYearId: baseFiscalYear.id,
      name: "Adjustment Period",
      startDate: new Date("2024-12-01"),
      endDate: new Date("2024-12-31"),
      periodType: "ADJUSTMENT",
      status: "NEVER_OPENED",
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([lastPeriodOfYear]);
    spies.insert.mockReturnValueOnce(adjustmentResult);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        fiscalYearId: baseFiscalYear.id,
        name: "Adjustment Period",
        startDate: new Date("2024-12-01"),
        endDate: new Date("2024-12-31"),
        periodType: "ADJUSTMENT",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(new Date(value.accountingPeriod.endDate).getTime()).toBe(
      new Date(lastPeriodOfYear.endDate).getTime(),
    );
  });

  it("creates periods for a non-calendar fiscal year with correct boundaries", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const nonCalendarFiscalYear = {
      ...baseFiscalYear,
      id: "fy-non-calendar",
      startDate: new Date("2024-04-01"),
      endDate: new Date("2025-03-31"),
    };

    const createdPeriod = {
      id: "period-apr",
      companyId: baseCompany.id,
      fiscalYearId: nonCalendarFiscalYear.id,
      name: "April 2024",
      startDate: new Date("2024-04-01"),
      endDate: new Date("2024-04-30"),
      periodType: "OPERATING",
      status: "NEVER_OPENED",
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(nonCalendarFiscalYear).mockReturnValueOnce([]);
    spies.insert.mockReturnValueOnce(createdPeriod);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        fiscalYearId: nonCalendarFiscalYear.id,
        name: "April 2024",
        startDate: new Date("2024-04-01"),
        endDate: new Date("2024-04-30"),
        periodType: "OPERATING",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.accountingPeriod.status).toBe("NEVER_OPENED");
  });

  it("periods are scoped to a company; periods from different companies are isolated", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const company2FiscalYear = {
      ...baseFiscalYear,
      id: "fy-company-2",
      companyId: "company-2",
    };

    const createdPeriod = {
      id: "period-company-2",
      companyId: "company-2",
      fiscalYearId: company2FiscalYear.id,
      name: "January 2024",
      startDate: new Date("2024-01-01"),
      endDate: new Date("2024-01-31"),
      periodType: "OPERATING",
      status: "NEVER_OPENED",
      createdAt: new Date(),
      updatedAt: null,
    };

    // Even though company-1 has a period with the same dates, company-2 should succeed
    spies.select.mockReturnValueOnce(company2FiscalYear).mockReturnValueOnce([]);
    spies.insert.mockReturnValueOnce(createdPeriod);

    const result = await run(
      db,
      {
        companyId: "company-2",
        fiscalYearId: company2FiscalYear.id,
        name: "January 2024",
        startDate: new Date("2024-01-01"),
        endDate: new Date("2024-01-31"),
        periodType: "OPERATING",
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.accountingPeriod.companyId).toBe("company-2");
  });

  it("emits audit event recording acting user, timestamp, and period details", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdPeriod = {
      id: "period-audit",
      companyId: baseCompany.id,
      fiscalYearId: baseFiscalYear.id,
      name: "January 2024",
      startDate: new Date("2024-01-01"),
      endDate: new Date("2024-01-31"),
      periodType: "OPERATING",
      status: "NEVER_OPENED",
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(baseFiscalYear).mockReturnValueOnce([]);
    spies.insert.mockReturnValueOnce(createdPeriod);

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

    // Verify the period was created successfully (audit is handled at the infrastructure level)
    const value = expectOk(result);
    expect(value.accountingPeriod).toBeDefined();
    expect(value.accountingPeriod.id).toBe("period-audit");
  });
});
