import Decimal from "decimal.js";
import { v4 as uuidv4 } from "uuid";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { Price } from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";

import { pruneDatabase } from "../../../__tests__/utils";
import { IngestionService } from "../../IngestionService";
import * as clickhouseWriteExports from "../../ClickhouseWriter";

const mockAddToClickhouseWriter = vi.fn();
const mockClickhouseClient = {
  query: async () => ({
    json: async () => [],
    query_id: "1",
    response_headers: { "x-clickhouse-summary": [] },
  }),
};

vi.mock("../../ClickhouseWriter", async (importOriginal) => {
  const original = (await importOriginal()) as {};
  return {
    ...original,
    ClickhouseWriter: {
      getInstance: () => ({
        addToQueue: mockAddToClickhouseWriter,
      }),
    },
  };
});

const mockIngestionService = new IngestionService(
  null as any,
  prisma,
  clickhouseWriteExports.ClickhouseWriter.getInstance() as any,
  mockClickhouseClient as any,
);

describe("Token Cost Calculation", () => {
  const modelName = "gpt-test-" + uuidv4();
  const matchPattern = `(?i)^(${modelName})$`;
  const traceId = uuidv4();
  const generationId = uuidv4();
  const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";

  const modelId = uuidv4();
  const tokenModelData = {
    id: modelId,
    modelName,
    matchPattern,
    tokenizerId: "openai",
    tokenizerConfig: {
      tokensPerName: 1,
      tokenizerModel: "gpt-4o",
      tokensPerMessage: 3,
    },
  };

  const modelPrices: Pick<Price, "price" | "usageType">[] = [
    {
      price: new Decimal(0.01),
      usageType: "input",
    },
    {
      price: new Decimal(0.02),
      usageType: "output",
    },
    {
      price: new Decimal(0.03),
      usageType: "total",
    },
  ];

  beforeEach(async () => {
    await pruneDatabase();
    const model = await prisma.model.create({
      data: tokenModelData,
    });
    const pricingTierId = uuidv4();
    await prisma.pricingTier.create({
      data: {
        id: pricingTierId,
        name: "Standard",
        isDefault: true,
        priority: 0,
        conditions: [],
        modelId,
      },
    });

    await Promise.all([
      prisma.price.createMany({
        data: modelPrices.map((price) => ({
          pricingTierId,
          modelId,
          projectId: null,
          usageType: price.usageType,
          price: price.price,
        })),
      }),
    ]);

    vi.clearAllMocks();
  });

  it("should correctly calculate token costs with provided model prices", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: 100,
      output: 200,
      total: 300,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBe(1.0); // 100 tokens * 0.01
    expect(costs.cost_details.output).toBe(4.0); // 200 tokens * 0.02
    expect(costs.cost_details.total).toBe(9.0); // 300 tokens * 0.03
    expect(costs.total_cost).toBe(9.0);
  });

  it("should correctly calculate token costs with user provided costs", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: 100,
      output: 200,
      total: 300,
    };

    const userProvidedCosts = {
      input: 2.0,
      output: 3.0,
      total: 5.0,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      { provided_cost_details: userProvidedCosts },
      usageUnits,
    );

    expect(costs.cost_details.input).toBe(2.0); // Overridden by user provided cost
    expect(costs.cost_details.output).toBe(3.0); // Overridden by user provided cost
    expect(costs.cost_details.total).toBe(5.0); // Overridden by user provided cost
    expect(costs.total_cost).toBe(5.0);
  });

  it("should correctly calculate token costs when only some user provided costs are given", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const data = [
      // missing total
      {
        userProvidedCosts: {
          input: 1,
          output: 2,
        },
        expectedCost: {
          input_cost: 1,
          output_cost: 2,
          total_cost: 3,
        },
      },
      // only total
      {
        userProvidedCosts: {
          total: 2,
        },
        expectedCost: {
          total_cost: 2,
        },
      },
      // missing input
      {
        userProvidedCosts: {
          output: 2,
          total: 2,
        },
        expectedCost: {
          output_cost: 2,
          total_cost: 2,
        },
      },
      // only input
      {
        userProvidedCosts: {
          input: 1,
        },
        expectedCost: {
          input_cost: 1,
          total_cost: 1,
        },
      },

      // missing output
      {
        userProvidedCosts: {
          input: 1,
          total: 1,
        },
        expectedCost: {
          input_cost: 1,
          output_cost: undefined,
          total_cost: 1,
        },
      },

      // only output
      {
        userProvidedCosts: {
          output: 2,
        },
        expectedCost: {
          input_cost: undefined,
          output_cost: 2,
          total_cost: 2,
        },
      },
    ];

    for (const { userProvidedCosts, expectedCost } of data) {
      const usageUnits = {
        input: 0,
        output: 0,
        total: 0,
      };

      const costs = (IngestionService as any).calculateUsageCosts(
        prices as any,
        { provided_cost_details: userProvidedCosts } as any,
        usageUnits,
      );

      expect(costs.cost_details.input).toBe(expectedCost.input_cost);
      expect(costs.cost_details.output).toBe(expectedCost.output_cost);
      expect(costs.cost_details.total).toBe(expectedCost.total_cost);
      expect(costs.total_cost).toBe(expectedCost.total_cost);
    }
  });

  it("should return empty costs if no model is provided", async () => {
    const usageUnits = {
      input: 100,
      output: 200,
      total: 300,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      null,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBeUndefined();
    expect(costs.cost_details.output).toBeUndefined();
    expect(costs.cost_details.total).toBeUndefined();
    expect(costs.total_cost).toBeUndefined();
  });

  it("should handle zero token counts correctly", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: 0,
      output: 0,
      total: 0,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBe(0); // 0 tokens * 0.01
    expect(costs.cost_details.output).toBe(0); // 0 tokens * 0.02
    expect(costs.cost_details.total).toBe(0); // 0 tokens * 0.03
    expect(costs.total_cost).toBe(0);
  });

  it("should handle missing token counts correctly", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: undefined,
      output: undefined,
      total: undefined,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBeUndefined();
    expect(costs.cost_details.output).toBeUndefined();
    expect(costs.cost_details.total).toBeUndefined();
    expect(costs.total_cost).toBeUndefined();
  });

  it("should handle fractional token counts correctly", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: 150.5,
      output: 250.25,
      total: 400.75,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBeCloseTo(1.505); // 150.5 tokens * 0.01
    expect(costs.cost_details.output).toBeCloseTo(5.005); // 250.25 tokens * 0.02
    expect(costs.cost_details.total).toBeCloseTo(12.0225); // 400.75 tokens * 0.03
    expect(costs.total_cost).toBeCloseTo(12.0225);
  });

  it("should handle large token counts correctly", async () => {
    const prices = await prisma.price.findMany({
      where: {
        modelId,
      },
    });

    const usageUnits = {
      input: 1e6,
      output: 2e6,
      total: 3e6,
    };

    const userProvidedCosts = {
      input: null,
      output: null,
      total: null,
    };

    const costs = (IngestionService as any).calculateUsageCosts(
      prices as any,
      userProvidedCosts,
      usageUnits,
    );

    expect(costs.cost_details.input).toBe(10000); // 1e6 tokens * 0.01
    expect(costs.cost_details.output).toBe(40000); // 2e6 tokens * 0.02
    expect(costs.cost_details.total).toBe(90000); // 3e6 tokens * 0.03
    expect(costs.total_cost).toBe(90000);
  });

  it("should correctly match model prices from the database", async () => {
    const generationUsage = {
      model: modelName,
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const generationId = uuidv4();

    const events = [
      {
        id: generationId,
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage.usage.input * modelPrices[0].price.toNumber(),
    );
    expect(generation.cost_details.output).toBe(
      generationUsage.usage.output * modelPrices[1].price.toNumber(),
    );
    expect(generation.cost_details.total).toBe(
      generationUsage.usage.total * modelPrices[2].price.toNumber(),
    );
    expect(generation.usage_details.input).toBe(generationUsage.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage.usage.total);
  });

  it("should overwrite costs for a generation without previous user provided costs", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: modelName,
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage2.usage.input * modelPrices[0].price.toNumber(),
    );
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.output * modelPrices[1].price.toNumber(),
    );
    expect(generation.cost_details.total).toBe(
      generationUsage2.usage.total * modelPrices[2].price.toNumber(),
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should correctly handle first manual tokenization and then provided tokens", async () => {
    const generationUsage1 = {
      model: modelName,
      input: "hello world",
      output: "",
      usage: null,
    };

    const generationUsage2 = {
      output: "whassup",
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage2.usage.input * modelPrices[0].price.toNumber(),
    );
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.output * modelPrices[1].price.toNumber(),
    );
    expect(generation.cost_details.total).toBe(
      generationUsage2.usage.total * modelPrices[2].price.toNumber(),
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should use the matched model of the previous generation call to calculate costs", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: undefined, // No model provided
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage2.usage.input * modelPrices[0].price.toNumber(),
    );
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.output * modelPrices[1].price.toNumber(),
    );
    expect(generation.cost_details.total).toBe(
      generationUsage2.usage.total * modelPrices[2].price.toNumber(),
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should overwrite costs if new costs are user provided", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: modelName,
      usage: {
        input: 100,
        output: 200,
        total: 300,
        inputCost: 1234,
        outputCost: 23523,
        totalCost: 5354,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // User provided cost
    expect(generation.provided_cost_details.input).toBe(
      generationUsage2.usage.inputCost,
    );
    expect(generation.provided_cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.provided_cost_details.total).toBe(
      generationUsage2.usage.totalCost,
    );

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage2.usage.inputCost,
    );
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.cost_details.total).toBe(
      generationUsage2.usage.totalCost,
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should overwrite costs if new costs are user provided and zero", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: modelName,
      usage: {
        input: 100,
        output: 200,
        total: 300,
        inputCost: 0,
        outputCost: 0,
        totalCost: 0,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // User provided cost
    expect(generation.provided_cost_details.input).toBe(
      generationUsage2.usage.inputCost,
    );
    expect(generation.provided_cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.provided_cost_details.total).toBe(
      generationUsage2.usage.totalCost,
    );

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage2.usage.inputCost,
    );
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.cost_details.total).toBe(
      generationUsage2.usage.totalCost,
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should overwrite costs if new costs are user provided and only partial", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: modelName,
      usage: {
        outputCost: 1,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // User provided cost
    expect(generation.provided_cost_details.input).toBe(undefined);
    expect(generation.provided_cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.provided_cost_details.total).toBe(undefined);

    // Calculated cost
    expect(generation.cost_details.input).toBe(undefined);
    expect(generation.cost_details.output).toBe(
      generationUsage2.usage.outputCost,
    );
    expect(generation.cost_details.total).toBe(1);
    expect(generation.usage_details.input).toBe(generationUsage1.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage1.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage1.usage.total);
  });

  it("should not overwrite costs if previous cost were user provided", async () => {
    const generationUsage1 = {
      model: modelName,
      usage: {
        input: 1,
        output: 2,
        total: 3,
        inputCost: 1234,
        outputCost: 23523,
        totalCost: 5354,
      },
    };

    const generationUsage2 = {
      model: modelName,
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched

    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // User provided cost
    expect(generation.provided_cost_details.input).toBe(
      generationUsage1.usage.inputCost,
    );
    expect(generation.provided_cost_details.output).toBe(
      generationUsage1.usage.outputCost,
    );
    expect(generation.provided_cost_details.total).toBe(
      generationUsage1.usage.totalCost,
    );

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage1.usage.inputCost,
    );
    expect(generation.cost_details.output).toBe(
      generationUsage1.usage.outputCost,
    );
    expect(generation.cost_details.total).toBe(
      generationUsage1.usage.totalCost,
    );
    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should not calculate anything if no costs are provided and no model is matched", async () => {
    const generationUsage1 = {
      model: undefined, // No model provided
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: undefined, // No model provided
      usage: {
        input: 100,
        output: 200,
        total: 300,
      },
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should not be matched
    expect(generation.internal_model).toBeUndefined();

    expect(generation.internal_model_id).toBeUndefined();

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // No calculated cost
    expect(generation.cost_details.input).toBeUndefined();
    expect(generation.cost_details.output).toBeUndefined();
    expect(generation.cost_details.total).toBeUndefined();

    expect(generation.usage_details.input).toBe(generationUsage2.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage2.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage2.usage.total);
  });

  it("should use the tokens of the previous call without model if model comes with following call", async () => {
    const generationUsage1 = {
      model: undefined, // No model provided
      usage: {
        input: 1,
        output: 2,
        total: 3,
      },
    };

    const generationUsage2 = {
      model: modelName,
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
      {
        id: uuidv4(),
        type: "generation-update",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage2,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });
    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");

    // Model name should be matched
    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // Calculated cost
    expect(generation.cost_details.input).toBe(
      generationUsage1.usage.input * modelPrices[0].price.toNumber(),
    );
    expect(generation.cost_details.output).toBe(
      generationUsage1.usage.output * modelPrices[1].price.toNumber(),
    );
    expect(generation.cost_details.total).toBe(
      generationUsage1.usage.total * modelPrices[2].price.toNumber(),
    );
    expect(generation.usage_details.input).toBe(generationUsage1.usage.input);
    expect(generation.usage_details.output).toBe(generationUsage1.usage.output);
    expect(generation.usage_details.total).toBe(generationUsage1.usage.total);
  });

  it("should skip tokenization and cost calculation if generation status is ERROR", async () => {
    const generationUsage1 = {
      model: modelName,
      input: "hello world",
      output: "hey whassup",
      usage: null,
      level: "ERROR",
    };

    const events = [
      {
        id: uuidv4(),
        type: "generation-create",
        timestamp: new Date().toISOString(),
        body: {
          id: generationId,
          startTime: new Date().toISOString(),
          ...generationUsage1,
        },
      },
    ];

    await (mockIngestionService as any).processObservationEventList({
      projectId,
      entityId: generationId,
      createdAtTimestamp: new Date(),
      observationEventList: events,
    });

    expect(mockAddToClickhouseWriter).toHaveBeenCalled();
    const args = mockAddToClickhouseWriter.mock.calls[0];
    const tableName = args[0];
    const generation = args[1];

    expect(tableName).toBe("observations");
    expect(generation).toBeDefined();
    expect(generation.type).toBe("GENERATION");
    expect(generation.level).toBe("ERROR");

    // Model name should be matched
    expect(generation.internal_model_id).toBe(tokenModelData.id);

    // No user provided cost
    expect(generation.provided_cost_details.input).toBeUndefined();
    expect(generation.provided_cost_details.output).toBeUndefined();
    expect(generation.provided_cost_details.total).toBeUndefined();

    // No calculated cost
    expect(generation.cost_details.input).toBeUndefined();
    expect(generation.cost_details.output).toBeUndefined();
    expect(generation.cost_details.total).toBeUndefined();

    expect(generation.usage_details.input).toBeUndefined();
    expect(generation.usage_details.output).toBeUndefined();
    expect(generation.usage_details.total).toBeUndefined();
  });

  describe("string to number conversion in getUsageUnits", () => {
    // These tests verify that usage_details values are correctly converted to numbers
    // even when they come in as strings (which can happen when reading from ClickHouse,
    // as the ClickHouse JS client may return UInt64 values as strings).
    // This prevents string concatenation bugs like "100" + "200" = "100200" instead of 300.

    it("should correctly convert string usage values to numbers and compute total", async () => {
      const generationId = uuidv4();

      // Simulate usageDetails coming from ClickHouse as strings
      const events = [
        {
          id: generationId,
          startTime: new Date().toISOString(),
          modelName,
          // These string values simulate what ClickHouse might return for UInt64
          providedUsageDetails: {
            input: "100" as unknown as number,
            output: "200" as unknown as number,
          },
        },
      ];

      await (mockIngestionService as any).writeEvent(events[0], "testfile.txt");

      expect(mockAddToClickhouseWriter).toHaveBeenCalled();
      const args = mockAddToClickhouseWriter.mock.calls[0];
      const generation = args[1];

      // Values should be numbers, not strings
      expect(typeof generation.usage_details.input).toBe("number");
      expect(typeof generation.usage_details.output).toBe("number");
      expect(typeof generation.usage_details.total).toBe("number");

      // Total should be numeric addition (300), not string concatenation ("100200")
      expect(generation.usage_details.input).toBe(100);
      expect(generation.usage_details.output).toBe(200);
      expect(generation.usage_details.total).toBe(300);
    });

    it("should ignore invalid string values that cannot be converted to numbers", async () => {
      const generationId = uuidv4();

      const events = [
        {
          id: generationId,
          startTime: new Date().toISOString(),
          modelName,
          // These string values simulate what ClickHouse might return for UInt64
          providedUsageDetails: {
            input: "100" as unknown as number,
            output: "non_a_number" as unknown as number,
          },
        },
      ];

      await (mockIngestionService as any).writeEvent(events[0], "testfile.txt");

      // Invalid values should be ignored
      expect(mockAddToClickhouseWriter).toHaveBeenCalled();
      const args = mockAddToClickhouseWriter.mock.calls[0];
      const generation = args[1];

      // Values should be numbers, not strings
      expect(typeof generation.usage_details.input).toBe("number");
      expect(typeof generation.usage_details.total).toBe("number");

      expect(generation.usage_details.input).toBe(100);
      expect(generation.usage_details.total).toBe(100);
    });
  });
});
