import type { QueryContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  CurrencyNotFoundError,
  ExchangeRateNotFoundError,
  InactiveCurrencyError,
} from "../lib/errors.generated";
import {
  baseCurrencyEUR,
  baseCurrencyJPY,
  baseCurrencyUSD,
  baseExchangeRateUSDtoEUR,
  baseExchangeRateUSDtoJPY,
  inactiveCurrency,
  olderExchangeRateUSDtoEUR,
} from "../testing/fixtures";
import { convertAmount } from "./convertAmount.generated";

describe("convertAmount", () => {
  const ctx: QueryContext = { actorId: "test-actor" };

  // Error cases first
  it("returns error when source currency doesn't exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: "XYZ",
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CurrencyNotFoundError);
    }
    expect(mock.selects).toHaveLength(1);
    expect(mock.selects[0].parameters).toContain("XYZ");
    expect(mock.selects[0].sql).toMatch(/"code"/);
  });

  it("returns error when target currency doesn't exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source exists
      [], // target doesn't exist
    );

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: "XYZ",
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CurrencyNotFoundError);
    }
    expect(mock.selects).toHaveLength(2);
    expect(mock.selects[1].parameters).toContain("XYZ");
  });

  it("returns error when source currency is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [inactiveCurrency], // source inactive
      [baseCurrencyEUR], // target exists
    );

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: inactiveCurrency.code,
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InactiveCurrencyError);
    }
    expect(mock.selects).toHaveLength(2);
  });

  it("returns error when target currency is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source exists
      [inactiveCurrency], // target inactive
    );

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: inactiveCurrency.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InactiveCurrencyError);
    }
    expect(mock.selects).toHaveLength(2);
  });

  it("returns error when no exchange rate exists for the currency pair", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source exists
      [baseCurrencyEUR], // target exists
      [], // no direct rate
      [], // no inverse rate
    );

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ExchangeRateNotFoundError);
    }
    expect(mock.selects).toHaveLength(4);
  });

  // Success cases
  it("returns same amount when source and target are the same currency", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseCurrencyUSD], [baseCurrencyUSD]);

    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyUSD.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(100);
      expect(result.value.exchangeRate).toBe(1);
    }
    expect(mock.selects).toHaveLength(2);
  });

  it("converts USD to EUR using direct rate", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source
      [baseCurrencyEUR], // target
      [baseExchangeRateUSDtoEUR], // rate = 0.92
    );

    // 100 USD * 0.92 = 92 EUR
    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(92);
      expect(result.value.exchangeRate).toBe(0.92);
    }
    expect(mock.selects).toHaveLength(3);
    expect(mock.selects[2].sql).toMatch(/"sourceCurrencyId"/);
    expect(mock.selects[2].sql).toMatch(/"targetCurrencyId"/);
    expect(mock.selects[2].sql).toMatch(/order by .*"effectiveDate" desc/i);
  });

  it("uses inverse rate when no direct rate exists", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyEUR], // source
      [baseCurrencyUSD], // target
      [], // no direct EUR->USD rate
      [baseExchangeRateUSDtoEUR], // inverse USD->EUR rate = 0.92
    );

    // 100 EUR * (1/0.92) = 108.70 USD (rounded to 2 decimal places)
    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyEUR.code,
        targetCurrencyCode: baseCurrencyUSD.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(108.7);
      expect(result.value.exchangeRate).toBeCloseTo(1 / 0.92, 5);
    }
    expect(mock.selects).toHaveLength(4);
  });

  it("uses most recent rate on or before the conversion date", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source
      [baseCurrencyEUR], // target
      [olderExchangeRateUSDtoEUR], // rate = 0.85 from 2024-01-01
    );

    // Querying for 2024-01-10, should use rate from 2024-01-01
    // 100 USD * 0.85 = 85 EUR
    const result = await convertAmount(
      mock.db,
      {
        amount: 100,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-10",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(85);
      expect(result.value.exchangeRate).toBe(0.85);
    }
    expect(mock.selects).toHaveLength(3);
    expect(mock.selects[2].sql).toMatch(/order by .*"effectiveDate" desc/i);
  });

  it("rounds to target currency decimal places (JPY has 0)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseCurrencyUSD], // source
      [baseCurrencyJPY], // target (0 decimal places)
      [baseExchangeRateUSDtoJPY], // rate = 148.5
    );

    // 100.50 USD * 148.5 = 14924.25 -> rounded to 14924 JPY
    const result = await convertAmount(
      mock.db,
      {
        amount: 100.5,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyJPY.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(14924);
    }
    expect(mock.selects).toHaveLength(3);
  });

  it("handles zero amount correctly", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseCurrencyUSD], [baseCurrencyEUR], [baseExchangeRateUSDtoEUR]);

    const result = await convertAmount(
      mock.db,
      {
        amount: 0,
        sourceCurrencyCode: baseCurrencyUSD.code,
        targetCurrencyCode: baseCurrencyEUR.code,
        conversionDate: "2024-01-20",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedAmount).toBe(0);
    }
    expect(mock.selects).toHaveLength(3);
  });
});
