import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  NameRequiredError,
  TemplateNotFoundError,
  TemplateProviderNotConfiguredError,
} from "../lib/errors.generated";
import { baseDraftCoa, baseActiveCompany } from "../testing/fixtures";
import { run, type TemplateProvider, type OrganizationQueries } from "./createChartOfAccounts";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

const ctx: CommandContext = {
  actorId: "test-actor",
  permissions: ["coa-management:createChartOfAccounts"],
};

const validInput = {
  name: "Standard CoA",
  companyId: "company-1",
};

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

    const result = await run(db, { ...validInput, companyId: "nonexistent" }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CompanyNotFoundError);
    }
  });

  it("returns error when company is not ACTIVE", async () => {
    const { db } = createMockDb<Transaction>();
    const draftCompany = { ...baseActiveCompany, status: "DRAFT" };
    const organizationQueries: Pick<OrganizationQueries, "getCompany"> = {
      getCompany: () =>
        Promise.resolve({
          ok: true as const,
          value: { company: draftCompany },
        }),
    };

    const result = await run(db, validInput, ctx, organizationQueries);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CompanyNotFoundError);
    }
  });

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

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

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(NameRequiredError);
    }
  });

  it("returns error when templateId is provided but no template provider is configured", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseActiveCompany); // company found

    const result = await run(
      db,
      { ...validInput, templateId: "template-1" },
      ctx,
      undefined,
      undefined,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(TemplateProviderNotConfiguredError);
    }
  });

  it("returns error when templateId does not reference a valid template", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseActiveCompany); // company found

    const templateProvider: TemplateProvider = {
      resolveTemplate: () => Promise.resolve(null),
    };

    const result = await run(
      db,
      { ...validInput, templateId: "nonexistent-template" },
      ctx,
      undefined,
      templateProvider,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(TemplateNotFoundError);
    }
  });

  it("creates blank CoA in DRAFT status when no templateId is provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      id: "new-coa-id",
    };
    spies.select.mockReturnValueOnce(baseActiveCompany); // company found
    spies.insert.mockReturnValue(createdCoa);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.chartOfAccounts.status).toBe("DRAFT");
      expect(result.value.chartOfAccounts.name).toBe("Standard CoA");
    }
  });

  it("creates CoA from template with pre-populated account groups and accounts in DRAFT status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      id: "new-coa-id",
    };
    const createdGroup = {
      id: "group-new-1",
      code: "1000",
      name: "Assets",
      chartOfAccountsId: "new-coa-id",
      parentAccountGroupId: null,
      sortOrder: 1,
      numberRangeStart: 1000,
      numberRangeEnd: 1999,
      createdAt: new Date(),
      updatedAt: null,
    };
    const createdAccount = {
      id: "account-new-1",
      code: "1100",
      name: "Cash",
      accountType: "ASSET",
      classification: "REGULAR",
      chartOfAccountsId: "new-coa-id",
      accountGroupId: "group-new-1",
      currencyRestriction: null,
      reconciliation: false,
      defaultTaxCode: null,
      tags: null,
      successorAccountId: null,
      hasPostedTransactions: false,
      status: "DRAFT",
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(baseActiveCompany); // company found
    spies.insert
      .mockReturnValueOnce(createdCoa) // CoA insert
      .mockReturnValueOnce(createdGroup) // group insert
      .mockReturnValueOnce(createdAccount); // account insert

    const templateProvider: TemplateProvider = {
      resolveTemplate: () =>
        Promise.resolve({
          accountGroups: [
            {
              code: "1000",
              name: "Assets",
              sortOrder: 1,
              numberRangeStart: 1000,
              numberRangeEnd: 1999,
            },
          ],
          accounts: [
            {
              code: "1100",
              name: "Cash",
              accountType: "ASSET" as const,
              classification: "REGULAR" as const,
              groupCode: "1000",
            },
          ],
        }),
    };

    const result = await run(
      db,
      { ...validInput, templateId: "template-jp" },
      ctx,
      undefined,
      templateProvider,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.chartOfAccounts.status).toBe("DRAFT");
    }
  });

  it("creates CoA from template with nested groups provided in child-first order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = { ...baseDraftCoa, id: "new-coa-id" };
    const createdParentGroup = {
      id: "group-parent",
      code: "1000",
      name: "Assets",
      chartOfAccountsId: "new-coa-id",
      parentAccountGroupId: null,
      sortOrder: 1,
      numberRangeStart: 1000,
      numberRangeEnd: 1999,
      createdAt: new Date(),
      updatedAt: null,
    };
    const createdChildGroup = {
      id: "group-child",
      code: "1100",
      name: "Current Assets",
      chartOfAccountsId: "new-coa-id",
      parentAccountGroupId: "group-parent",
      sortOrder: 1,
      numberRangeStart: 1100,
      numberRangeEnd: 1199,
      createdAt: new Date(),
      updatedAt: null,
    };

    spies.select.mockReturnValueOnce(baseActiveCompany); // company found
    spies.insert
      .mockReturnValueOnce(createdCoa) // CoA insert
      .mockReturnValueOnce(createdParentGroup) // parent group insert (sorted first)
      .mockReturnValueOnce(createdChildGroup); // child group insert (sorted second)

    // Template provides child BEFORE parent — implementation must sort
    const templateProvider: TemplateProvider = {
      resolveTemplate: () =>
        Promise.resolve({
          accountGroups: [
            {
              code: "1100",
              name: "Current Assets",
              parentCode: "1000",
              sortOrder: 1,
              numberRangeStart: 1100,
              numberRangeEnd: 1199,
            },
            {
              code: "1000",
              name: "Assets",
              sortOrder: 1,
              numberRangeStart: 1000,
              numberRangeEnd: 1999,
            },
          ],
          accounts: [],
        }),
    };

    const result = await run(
      db,
      { ...validInput, templateId: "template-nested" },
      ctx,
      undefined,
      templateProvider,
    );

    expect(result.ok).toBe(true);
    // Verify parent was inserted before child by checking insert order
    const insertCalls = spies.values.mock.calls;
    // First insert is the CoA itself, second is parent group, third is child group
    const parentInsert = insertCalls[1]?.[0] as Record<string, unknown>;
    const childInsert = insertCalls[2]?.[0] as Record<string, unknown>;
    expect(parentInsert.code).toBe("1000");
    expect(parentInsert.parentAccountGroupId).toBeNull();
    expect(childInsert.code).toBe("1100");
    expect(childInsert.parentAccountGroupId).toBe("group-parent");
  });

  it("creates CoA with optional description", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      description: "My custom description",
    };
    spies.select.mockReturnValueOnce(baseActiveCompany); // company found
    spies.insert.mockReturnValue(createdCoa);

    const result = await run(db, { ...validInput, description: "My custom description" }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.chartOfAccounts.description).toBe("My custom description");
    }
  });

  it("passes custom fields through to insert", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      customField: "custom-value",
    };
    spies.select.mockReturnValueOnce(baseActiveCompany); // company found
    spies.insert.mockReturnValue(createdCoa);

    const result = await run(
      db,
      { ...validInput, customField: "custom-value" } as CreateChartOfAccountsInputWithCF,
      ctx,
    );

    expect(result.ok).toBe(true);
    // Verify values spy was called with custom fields
    expect(spies.values).toHaveBeenCalled();
    const insertedValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(insertedValues.customField).toBe("custom-value");
  });

  it("validates company via organizationQueries when provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      id: "new-coa-id",
    };
    spies.insert.mockReturnValue(createdCoa);

    const organizationQueries: Pick<OrganizationQueries, "getCompany"> = {
      getCompany: () =>
        Promise.resolve({
          ok: true as const,
          value: { company: baseActiveCompany },
        }),
    };

    const result = await run(db, validInput, ctx, organizationQueries);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.chartOfAccounts.status).toBe("DRAFT");
    }
  });

  it("returns error when organizationQueries reports company not found", async () => {
    const { db } = createMockDb<Transaction>();

    const organizationQueries: Pick<OrganizationQueries, "getCompany"> = {
      getCompany: () =>
        Promise.resolve({
          ok: true as const,
          value: { company: null },
        }),
    };

    const result = await run(db, validInput, ctx, organizationQueries);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CompanyNotFoundError);
    }
  });

  it("creates blank CoA in DRAFT status with inherited base currency when no templateId is provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = {
      ...baseDraftCoa,
      id: "new-coa-id",
      baseCurrencyId: "currency-usd",
    };
    const organizationQueries: Pick<OrganizationQueries, "getCompany"> = {
      getCompany: () =>
        Promise.resolve({
          ok: true as const,
          value: { company: baseActiveCompany },
        }),
    };
    spies.insert.mockReturnValue(createdCoa);

    const result = await run(db, validInput, ctx, organizationQueries);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.chartOfAccounts.baseCurrencyId).toBe("currency-usd");
    }
    // Verify baseCurrencyId was passed to insert
    expect(spies.values).toHaveBeenCalled();
    const insertedValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(insertedValues.baseCurrencyId).toBe("currency-usd");
  });

  it("template-copied entities are mutable and can be modified before activation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdCoa = { ...baseDraftCoa, id: "new-coa-id" };
    const createdGroup = {
      id: "group-new-1",
      code: "1000",
      name: "Assets",
      chartOfAccountsId: "new-coa-id",
      parentAccountGroupId: null,
      sortOrder: 1,
      numberRangeStart: 1000,
      numberRangeEnd: 1999,
      createdAt: new Date(),
      updatedAt: null,
    };
    spies.select.mockReturnValueOnce(baseActiveCompany);
    spies.insert.mockReturnValueOnce(createdCoa).mockReturnValueOnce(createdGroup);

    const templateProvider = {
      resolveTemplate: () =>
        Promise.resolve({
          accountGroups: [
            {
              code: "1000",
              name: "Assets",
              sortOrder: 1,
              numberRangeStart: 1000,
              numberRangeEnd: 1999,
            },
          ],
          accounts: [],
        }),
    };

    const result = await run(
      db,
      { ...validInput, templateId: "t-1" },
      ctx,
      undefined,
      templateProvider,
    );

    expect(result.ok).toBe(true);
    // Template-created CoA is in DRAFT status, indicating entities are mutable
    if (result.ok) {
      expect(result.value.chartOfAccounts.status).toBe("DRAFT");
    }
  });
});

// Helper type for custom fields test
interface CreateChartOfAccountsInputWithCF {
  name: string;
  companyId: string;
  description?: string | null;
  templateId?: string;
  customField: string;
  [key: string]: unknown;
}
