import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountNotFoundError,
  ChartOfAccountsArchivedError,
  NameRequiredError,
  InvalidAccountCodeError,
  DuplicateAccountCodeError,
  StructuralFieldImmutableError,
  IncompatibleClassificationError,
  DuplicateRetainedEarningsError,
  ReconciliationRequiredError,
  CurrencyNotFoundError,
  AccountGroupNotFoundError,
  CodeOutOfRangeError,
} from "../lib/errors.generated";
import {
  baseDraftAccount,
  baseActiveAccount,
  baseAccountWithPostedTransactions,
  baseRetainedEarningsAccount,
  baseActiveCoa,
  baseArchivedCoa,
  baseChildGroup,
  baseRootGroup,
  baseGroupNoRange,
} from "../testing/fixtures";
import { run, type UpdateAccountInput } from "./updateAccount";
import type { CommandContext } from "@tailor-platform/erp-kit/module";

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

const mockGetCurrency = {
  getCurrency: (_db: Transaction, input: { id: string }, _ctx: CommandContext) =>
    Promise.resolve({
      ok: true as const,
      value: { currency: { id: input.id, isActive: true } },
    }),
};

const mockGetCurrencyNotFound = {
  getCurrency: (_db: Transaction, _input: { id: string }, _ctx: CommandContext) =>
    Promise.resolve({
      ok: true as const,
      value: { currency: null },
    }),
};

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

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

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

  it("returns error when parent CoA is ARCHIVED", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const accountInArchivedCoa = { ...baseDraftAccount, chartOfAccountsId: baseArchivedCoa.id };
    spies.select.mockReturnValueOnce(accountInArchivedCoa).mockReturnValueOnce(baseArchivedCoa);

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

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

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

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

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

  it("returns error when new account code is not a valid numeric identifier", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);

    const result = await run(db, { id: baseDraftAccount.id, code: "ABC" }, ctx);

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

  it("returns error when new account code already exists in the CoA", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // account lookup
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(baseActiveAccount); // duplicate code check

    const result = await run(db, { id: baseDraftAccount.id, code: baseActiveAccount.code }, ctx);

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

  it("returns error when changing structural field on account with posted transactions", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseAccountWithPostedTransactions)
      .mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: baseAccountWithPostedTransactions.id,
        code: "9999",
      },
      ctx,
    );

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

  it("returns error when new classification is incompatible with type", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        classification: "PAYABLE", // requires LIABILITY, but account is ASSET
      },
      ctx,
    );

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

  it("returns error when changing to RETAINED_EARNINGS and another account already has it", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const equityAccount = {
      ...baseDraftAccount,
      accountType: "EQUITY" as const,
      classification: "REGULAR" as const,
    };
    spies.select
      .mockReturnValueOnce(equityAccount) // account lookup
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(baseRetainedEarningsAccount); // existing retained earnings

    const result = await run(
      db,
      {
        id: equityAccount.id,
        classification: "RETAINED_EARNINGS",
      },
      ctx,
    );

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

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

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        currencyRestriction: "nonexistent-currency",
      },
      ctx,
      mockGetCurrencyNotFound,
    );

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

  it("returns error when new group belongs to different CoA", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const differentCoaGroup = { ...baseRootGroup, chartOfAccountsId: "coa-999" };
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // account lookup
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(differentCoaGroup); // group lookup

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        accountGroupId: differentCoaGroup.id,
      },
      ctx,
    );

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

  it("updates name of an account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, name: "Updated Name" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(db, { id: baseDraftAccount.id, name: "Updated Name" }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.name).toBe("Updated Name");
    }
  });

  it("updates tags of an account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, tags: "new-tag" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(db, { id: baseDraftAccount.id, tags: "new-tag" }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.tags).toBe("new-tag");
    }
  });

  it("updates default tax code", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, defaultTaxCode: "VAT10" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(db, { id: baseDraftAccount.id, defaultTaxCode: "VAT10" }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.defaultTaxCode).toBe("VAT10");
    }
  });

  it("updates account group assignment", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, accountGroupId: baseGroupNoRange.id };
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // account lookup
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(baseGroupNoRange); // group lookup
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        accountGroupId: baseGroupNoRange.id,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.accountGroupId).toBe(baseGroupNoRange.id);
    }
  });

  it("updates structural fields when no posted transactions exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, code: "1999", accountType: "LIABILITY" };
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // account lookup (no posted transactions)
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(undefined); // no duplicate code
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        code: "1999",
        accountType: "LIABILITY",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
  });

  it("returns error when setting isReconciliation to false on a RECEIVABLE account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseActiveAccount) // RECEIVABLE account
      .mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: baseActiveAccount.id,
        reconciliation: false,
      },
      ctx,
    );

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

  it("returns error when setting isReconciliation to false on a PAYABLE account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const payableAccount = {
      ...baseDraftAccount,
      classification: "PAYABLE" as const,
      accountType: "LIABILITY" as const,
    };
    spies.select.mockReturnValueOnce(payableAccount).mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: payableAccount.id,
        reconciliation: false,
      },
      ctx,
    );

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

  it("returns error when setting isReconciliation to false on a BANK_CASH account", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bankAccount = { ...baseDraftAccount, classification: "BANK_CASH" as const };
    spies.select.mockReturnValueOnce(bankAccount).mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: bankAccount.id,
        reconciliation: false,
      },
      ctx,
    );

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

  it("returns error when changing classification to RECEIVABLE with isReconciliation explicitly set to false", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // REGULAR account
      .mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        classification: "RECEIVABLE",
        reconciliation: false,
      },
      ctx,
    );

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

  it("enforces reconciliation when classification changes to RECEIVABLE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, classification: "RECEIVABLE", reconciliation: true };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        classification: "RECEIVABLE",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    // Verify reconciliation is set to true in the update
    expect(spies.set).toHaveBeenCalled();
    const setData = spies.set.mock.calls[0][0] as Record<string, unknown>;
    expect(setData.reconciliation).toBe(true);
  });

  it("marks account as excluded from posting when classification changes to NON_POSTING", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, classification: "NON_POSTING" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        classification: "NON_POSTING",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.classification).toBe("NON_POSTING");
    }
  });

  it("marks account as control account blocking manual postings when classification changes to RECEIVABLE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, classification: "RECEIVABLE", reconciliation: true };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        classification: "RECEIVABLE",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.classification).toBe("RECEIVABLE");
    }
  });

  it("marks account as control account blocking manual postings when classification changes to PAYABLE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const liabilityAccount = { ...baseDraftAccount, accountType: "LIABILITY" as const };
    const updated = { ...liabilityAccount, classification: "PAYABLE", reconciliation: true };
    spies.select.mockReturnValueOnce(liabilityAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: liabilityAccount.id,
        classification: "PAYABLE",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.classification).toBe("PAYABLE");
    }
  });

  it("removes posting exclusion when classification changes from NON_POSTING to REGULAR", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const nonPostingAccount = { ...baseDraftAccount, classification: "NON_POSTING" as const };
    const updated = { ...nonPostingAccount, classification: "REGULAR" };
    spies.select.mockReturnValueOnce(nonPostingAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: nonPostingAccount.id,
        classification: "REGULAR",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.classification).toBe("REGULAR");
    }
  });

  it("updates currency restriction and posting currency constraint accordingly", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, currencyRestriction: "currency-eur" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        currencyRestriction: "currency-eur",
      },
      ctx,
      mockGetCurrency,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.account.currencyRestriction).toBe("currency-eur");
    }
  });

  it("returns error when account code outside new group number range", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseDraftAccount) // account with code "1100"
      .mockReturnValueOnce(baseActiveCoa)
      .mockReturnValueOnce(baseRootGroup); // group with range 1000-1999 — but we'll use a tight range group

    // Use a group where 1100 is outside its range
    const tightGroup = { ...baseChildGroup, numberRangeStart: 1200, numberRangeEnd: 1299 };
    spies.select.mockReset();
    spies.select
      .mockReturnValueOnce(baseDraftAccount)
      .mockReturnValueOnce(baseActiveCoa)
      .mockReturnValueOnce(tightGroup); // group with range 1200-1299

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        accountGroupId: tightGroup.id,
      },
      ctx,
    );

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

  it("returns error when changing code to a value outside the current group's number range", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const accountInGroup = { ...baseDraftAccount, accountGroupId: baseChildGroup.id, code: "1150" };
    spies.select
      .mockReturnValueOnce(accountInGroup) // account lookup
      .mockReturnValueOnce(baseActiveCoa) // CoA lookup
      .mockReturnValueOnce(undefined) // no duplicate code
      .mockReturnValueOnce(baseChildGroup); // current group lookup (range 1100-1199)

    const result = await run(
      db,
      {
        id: accountInGroup.id,
        code: "9999", // outside range 1100-1199
      },
      ctx,
    );

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

  it("returns error when changing classification from RECEIVABLE after posted transactions", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receivableWithPosted = {
      ...baseAccountWithPostedTransactions,
      classification: "RECEIVABLE" as const,
    };
    spies.select.mockReturnValueOnce(receivableWithPosted).mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: receivableWithPosted.id,
        classification: "REGULAR",
      },
      ctx,
    );

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

  it("returns error when changing classification from PAYABLE after posted transactions", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const payableWithPosted = {
      ...baseAccountWithPostedTransactions,
      classification: "PAYABLE" as const,
      accountType: "LIABILITY" as const,
    };
    spies.select.mockReturnValueOnce(payableWithPosted).mockReturnValueOnce(baseActiveCoa);

    const result = await run(
      db,
      {
        id: payableWithPosted.id,
        classification: "REGULAR",
      },
      ctx,
    );

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

  it("passes custom fields through to update", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...baseDraftAccount, customField: "custom-value" };
    spies.select.mockReturnValueOnce(baseDraftAccount).mockReturnValueOnce(baseActiveCoa);
    spies.update.mockReturnValue(updated);

    const result = await run(
      db,
      {
        id: baseDraftAccount.id,
        customField: "custom-value",
      } as UpdateAccountInput & { customField: string },
      ctx,
    );

    expect(result.ok).toBe(true);
    expect(spies.set).toHaveBeenCalled();
    const setData = spies.set.mock.calls[0][0] as Record<string, unknown>;
    expect(setData.customField).toBe("custom-value");
  });
});
