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 {
  InactiveUnitError,
  IncompatibleUnitsError,
  UnitNotFoundError,
} from "../lib/errors.generated";
import {
  baseUnitGram,
  baseUnitKg,
  baseUnitLiter,
  baseUnitPound,
  inactiveUnit,
} from "../testing/fixtures";
import { convertQuantity } from "./convertQuantity.generated";

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

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

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

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

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

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

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

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

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

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

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

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

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

  it("returns error when units belong to different categories", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseUnitKg], // Weight category
      [baseUnitLiter], // Volume category
    );

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

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

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

    const result = await convertQuantity(
      mock.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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseUnitKg], // 1 kg = 1.0 (reference)
      [baseUnitGram], // 1 g = 0.001 kg
    );

    // 5 kg * 1.0 / 0.001 = 5000 g
    const result = await convertQuantity(
      mock.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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseUnitGram], // 1 g = 0.001 kg
      [baseUnitKg], // 1 kg = 1.0 (reference)
    );

    // 2500 g * 0.001 / 1.0 = 2.5 kg
    const result = await convertQuantity(
      mock.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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseUnitPound], // 1 lb = 0.453592 kg
      [baseUnitKg], // 1 kg = 1.0 (reference)
    );

    // 10 lb * 0.453592 / 1.0 = 4.53592 kg, rounded to 4.54
    const result = await convertQuantity(
      mock.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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseUnitKg], [baseUnitGram]);

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

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