import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import { DB } from "../generated/kysely-tailordb";
import {
  InactiveUnitError,
  IncompatibleUnitsError,
  UnitNotFoundError,
} from "../lib/errors.generated";
import {
  baseUnitGram,
  baseUnitKg,
  baseUnitLiter,
  baseUnitPound,
  inactiveUnit,
} from "../testing/fixtures";
import { convertQuantity } from "./convertQuantity.generated";
import type { QueryContext } from "@tailor-platform/erp-kit/module";

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

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

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: "xyz",
        targetUnitSymbol: baseUnitGram.symbol,
      },
      ctx,
    );

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

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

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: "xyz",
      },
      ctx,
    );

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

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

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: inactiveUnit.symbol,
        targetUnitSymbol: baseUnitGram.symbol,
      },
      ctx,
    );

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

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

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: inactiveUnit.symbol,
      },
      ctx,
    );

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

  it("returns error when units belong to different categories", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseUnitKg) // Weight category
      .mockReturnValueOnce(baseUnitLiter); // Volume category

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: baseUnitLiter.symbol,
      },
      ctx,
    );

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

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

    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: baseUnitKg.symbol,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedQuantity).toBe(10);
      expect(result.value.sourceUnit).toEqual(baseUnitKg);
      expect(result.value.targetUnit).toEqual(baseUnitKg);
    }
  });

  it("converts kg to grams correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseUnitKg) // 1 kg = 1.0 (reference)
      .mockReturnValueOnce(baseUnitGram); // 1 g = 0.001 kg

    // 5 kg * 1.0 / 0.001 = 5000 g
    const result = await convertQuantity(
      db,
      {
        quantity: 5,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: baseUnitGram.symbol,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedQuantity).toBe(5000); // rounded to 0 decimal places (gram precision)
    }
  });

  it("converts grams to kg correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseUnitGram) // 1 g = 0.001 kg
      .mockReturnValueOnce(baseUnitKg); // 1 kg = 1.0 (reference)

    // 2500 g * 0.001 / 1.0 = 2.5 kg
    const result = await convertQuantity(
      db,
      {
        quantity: 2500,
        sourceUnitSymbol: baseUnitGram.symbol,
        targetUnitSymbol: baseUnitKg.symbol,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedQuantity).toBe(2.5); // rounded to 2 decimal places (kg precision)
    }
  });

  it("converts pounds to kg correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select
      .mockReturnValueOnce(baseUnitPound) // 1 lb = 0.453592 kg
      .mockReturnValueOnce(baseUnitKg); // 1 kg = 1.0 (reference)

    // 10 lb * 0.453592 / 1.0 = 4.53592 kg, rounded to 4.54
    const result = await convertQuantity(
      db,
      {
        quantity: 10,
        sourceUnitSymbol: baseUnitPound.symbol,
        targetUnitSymbol: baseUnitKg.symbol,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedQuantity).toBe(4.54);
    }
  });

  it("handles zero quantity correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(baseUnitKg).mockReturnValueOnce(baseUnitGram);

    const result = await convertQuantity(
      db,
      {
        quantity: 0,
        sourceUnitSymbol: baseUnitKg.symbol,
        targetUnitSymbol: baseUnitGram.symbol,
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.convertedQuantity).toBe(0);
    }
  });
});
