import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import { DB } 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";
import type { QueryContext } from "@tailor-platform/erp-kit/module";

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

  // Error cases first
  it("returns error when source currency doesn't exist", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValue(undefined);

    const result = await convertAmount(
      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);
    }
  });

  it("returns error when target currency doesn't exist", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source exists
      .mockReturnValueOnce(undefined); // target doesn't exist

    const result = await convertAmount(
      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);
    }
  });

  it("returns error when source currency is inactive", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(inactiveCurrency) // source inactive
      .mockReturnValueOnce(baseCurrencyEUR); // target exists

    const result = await convertAmount(
      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);
    }
  });

  it("returns error when target currency is inactive", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source exists
      .mockReturnValueOnce(inactiveCurrency); // target inactive

    const result = await convertAmount(
      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);
    }
  });

  it("returns error when no exchange rate exists for the currency pair", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source exists
      .mockReturnValueOnce(baseCurrencyEUR) // target exists
      .mockReturnValueOnce(undefined) // no direct rate
      .mockReturnValueOnce(undefined); // no inverse rate

    const result = await convertAmount(
      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);
    }
  });

  // Success cases
  it("returns same amount when source and target are the same currency", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(baseCurrencyUSD).mockReturnValueOnce(baseCurrencyUSD);

    const result = await convertAmount(
      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);
    }
  });

  it("converts USD to EUR using direct rate", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source
      .mockReturnValueOnce(baseCurrencyEUR) // target
      .mockReturnValueOnce(baseExchangeRateUSDtoEUR); // rate = 0.92

    // 100 USD * 0.92 = 92 EUR
    const result = await convertAmount(
      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);
    }
  });

  it("uses inverse rate when no direct rate exists", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyEUR) // source
      .mockReturnValueOnce(baseCurrencyUSD) // target
      .mockReturnValueOnce(undefined) // no direct EUR->USD rate
      .mockReturnValueOnce(baseExchangeRateUSDtoEUR); // inverse USD->EUR rate = 0.92

    // 100 EUR * (1/0.92) = 108.70 USD (rounded to 2 decimal places)
    const result = await convertAmount(
      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);
    }
  });

  it("uses most recent rate on or before the conversion date", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source
      .mockReturnValueOnce(baseCurrencyEUR) // target
      .mockReturnValueOnce(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(
      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);
    }
  });

  it("rounds to target currency decimal places (JPY has 0)", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD) // source
      .mockReturnValueOnce(baseCurrencyJPY) // target (0 decimal places)
      .mockReturnValueOnce(baseExchangeRateUSDtoJPY); // rate = 148.5

    // 100.50 USD * 148.5 = 14924.25 -> rounded to 14924 JPY
    const result = await convertAmount(
      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);
    }
  });

  it("handles zero amount correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseCurrencyUSD)
      .mockReturnValueOnce(baseCurrencyEUR)
      .mockReturnValueOnce(baseExchangeRateUSDtoEUR);

    const result = await convertAmount(
      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);
    }
  });
});
