import { describe, expect, test } from "vitest";

import { SchemaError } from "@aigxion/error";
import { BillingTier, calculateTieredPrices } from "./tiers";

describe("calculateTieredPrices", () => {
  const testCases: {
    name: string;
    tiers: BillingTier[];
    units: number;
    expected: number;
  }[] = [
      {
        name: "only reaches the first tier",
        tiers: [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "100" },
          { firstUnit: 11, lastUnit: 20, centsPerUnit: "50" },
          { firstUnit: 21, lastUnit: null, centsPerUnit: "25" },
        ],
        units: 5,
        expected: 500,
      },
      {
        name: "only reaches the second tier",
        tiers: [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "100" },
          { firstUnit: 11, lastUnit: 20, centsPerUnit: "50" },
          { firstUnit: 21, lastUnit: null, centsPerUnit: "25" },
        ],
        units: 15,
        expected: 1250,
      },
      {
        name: "reaches the third tier",
        tiers: [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "100" },
          { firstUnit: 11, lastUnit: 20, centsPerUnit: "50" },
          { firstUnit: 21, lastUnit: null, centsPerUnit: "25" },
        ],
        units: 25,
        expected: 1625, // 10*1 + 10*0.5 + 5*0.25
      },
      {
        name: "single tier",
        tiers: [{ firstUnit: 1, lastUnit: null, centsPerUnit: "32.3" }],
        units: 12125,
        expected: 391637.5,
      },
      {
        name: "real world usage",
        tiers: [
          { firstUnit: 1, lastUnit: 2_500, centsPerUnit: null },
          { firstUnit: 2_501, lastUnit: 100_000, centsPerUnit: "0.02" },
          { firstUnit: 100_001, lastUnit: 500_000, centsPerUnit: "0.015" },
          { firstUnit: 500_001, lastUnit: 1_000_000, centsPerUnit: "0.01" },
          { firstUnit: 1_000_001, lastUnit: null, centsPerUnit: "0.005" },
        ],
        units: 3899437,
        expected: 27447.185,
      },
    ];
  for (const tc of testCases) {
    test(tc.name, () => {
      const result = calculateTieredPrices(tc.tiers, tc.units);
      expect(result.err).toBeUndefined();
      expect(result.val).toBeDefined();
      expect(result.val!.totalCentsEstimate).toBeCloseTo(tc.expected, 9);
    });
  }
});

describe("invalid tiers", () => {
  describe("empty array", () => {
    test("should fail", () => {
      const result = calculateTieredPrices([], 5);
      expect(result.err).toBeDefined();
      expect(result.err).instanceOf(SchemaError);

      expect(result.err?.message).toEqual("too_small: : Array must contain at least 1 element(s)");
    });
  });

  describe("there is a gap between tiers", () => {
    test("should fail", () => {
      const result = calculateTieredPrices(
        [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "50" },
          { firstUnit: 12, lastUnit: 20, centsPerUnit: "100" },
        ],
        20,
      );
      expect(result.err).toBeDefined();
      expect(result.err).instanceOf(SchemaError);
      expect(result.err?.message).toEqual("There is a gap between tiers");
    });
  });
  describe("two tiers overlap", () => {
    test("should fail", () => {
      const result = calculateTieredPrices(
        [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "100" },
          { firstUnit: 10, lastUnit: null, centsPerUnit: "50" },
        ],
        20,
      );
      expect(result.err).toBeDefined();
      expect(result.err).instanceOf(SchemaError);

      expect(result.err?.message).toEqual("There is an overlap between tiers");
    });
  });
  describe("two tiers have a gap between", () => {
    test("should fail", () => {
      const result = calculateTieredPrices(
        [
          { firstUnit: 1, lastUnit: 10, centsPerUnit: "100" },
          { firstUnit: 12, lastUnit: null, centsPerUnit: "50" },
        ],
        20,
      );
      expect(result.err).toBeDefined();
      expect(result.err).instanceOf(SchemaError);
      expect(result.err!.message).toEqual("There is a gap between tiers");
    });
  });

  describe("any tier but the last has no lastUnit set", () => {
    test("should fail", () => {
      const { val, err } = calculateTieredPrices(
        [
          { firstUnit: 1, lastUnit: null, centsPerUnit: "100" },
          { firstUnit: 11, lastUnit: null, centsPerUnit: "50" },
        ],
        20,
      );
      expect(val).toBeUndefined();
      expect(err!.message).toEqual("Every tier except the last one must have a lastUnit");
    });
  });
});
