import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { GetCurrencyInput } from "../../primitives/query/getCurrency";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  BankAccountNotFoundError,
  InvalidBankNameError,
  InvalidAccountHolderNameError,
  CurrencyNotFoundError,
  MissingAccountIdentifierError,
} from "../lib/errors.generated";
import {
  baseDefaultBankAccount,
  baseSecondBankAccount,
  baseDraftPartner,
  baseActivePartner,
  baseInactivePartner,
} from "../testing/fixtures";
import { run } from "./updatePartnerBankAccount";

describe("updatePartnerBankAccount", () => {
  const ctx: CommandContext = {
    actorId: "test-actor",
    permissions: ["business-partner:partner:updatePartnerBankAccount"],
  };

  const baseCurrency = {
    id: "currency-usd",
    code: "USD",
    name: "US Dollar",
    symbol: "$",
    decimalPlaces: 2,
    status: "ACTIVE" as const,
    isBaseCurrency: true,
    createdAt: new Date("2024-01-01"),
    updatedAt: null,
  };

  const mockGetCurrency = (_db: unknown, input: GetCurrencyInput, _ctx: unknown) =>
    Promise.resolve({
      ok: true as const,
      value: {
        currency: "id" in input && input.id === "currency-usd" ? baseCurrency : null,
      },
    });
  const primitivesQueries = { getCurrency: mockGetCurrency };

  it("updates bank account details", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedAccount = { ...baseDefaultBankAccount, bankName: "Updated Bank" };
    mock.enqueueResults([baseDefaultBankAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", bankName: "Updated Bank" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.bankName).toBe("Updated Bank");
    }
  });

  it("updates routingNumber", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedAccount = { ...baseDefaultBankAccount, routingNumber: "021000089" };
    mock.enqueueResults([baseDefaultBankAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", routingNumber: "021000089" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.routingNumber).toBe("021000089");
    }
  });

  it("updates swiftBic", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedAccount = { ...baseDefaultBankAccount, swiftBic: "BOFAUS3N" };
    mock.enqueueResults([baseDefaultBankAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", swiftBic: "BOFAUS3N" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.swiftBic).toBe("BOFAUS3N");
    }
  });

  it("clears routingNumber when null is passed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedAccount = { ...baseDefaultBankAccount, routingNumber: null };
    mock.enqueueResults([baseDefaultBankAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", routingNumber: null }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.routingNumber).toBeNull();
    }
  });

  it("clears swiftBic when null is passed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updatedAccount = { ...baseSecondBankAccount, swiftBic: null };
    mock.enqueueResults([baseSecondBankAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-2", swiftBic: null }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.swiftBic).toBeNull();
    }
  });

  it("updates bank account for DRAFT partner", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const draftAccount = { ...baseDefaultBankAccount, partnerId: baseDraftPartner.id };
    const updatedAccount = { ...draftAccount, bankName: "Draft Bank" };
    mock.enqueueResults([draftAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", bankName: "Draft Bank" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.bankName).toBe("Draft Bank");
    }
  });

  it("updates bank account for ACTIVE partner", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const activeAccount = { ...baseDefaultBankAccount, partnerId: baseActivePartner.id };
    const updatedAccount = { ...activeAccount, bankName: "Active Bank" };
    mock.enqueueResults([activeAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", bankName: "Active Bank" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.bankName).toBe("Active Bank");
    }
  });

  it("updates bank account for INACTIVE partner", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const inactiveAccount = { ...baseDefaultBankAccount, partnerId: baseInactivePartner.id };
    const updatedAccount = { ...inactiveAccount, bankName: "Inactive Bank" };
    mock.enqueueResults([inactiveAccount], [updatedAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", bankName: "Inactive Bank" }, ctx, primitivesQueries),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.bankAccount.bankName).toBe("Inactive Bank");
    }
  });

  it("returns error when account not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(trx, { id: "nonexistent" }, ctx, primitivesQueries),
    );

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

  it("returns error when bank name is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseDefaultBankAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", bankName: "" }, ctx, primitivesQueries),
    );

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

  it("returns error when account holder name is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseDefaultBankAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", accountHolderName: "" }, ctx, primitivesQueries),
    );

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

  it("returns error when currency not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseDefaultBankAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", currencyId: "currency-nonexistent" }, ctx, primitivesQueries),
    );

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

  it("returns error when update would remove all identifiers", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // Account has accountNumber but no iban
    mock.enqueueResults([baseDefaultBankAccount]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "bank-1", accountNumber: "", iban: "" }, ctx, primitivesQueries),
    );

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