/**
 * Usage utilities tests
 */

import { describe, expect, test } from "bun:test";
import {
  addUsage,
  cacheHitRatio,
  calculateUsageStats,
  emptyUsageStats,
  sanitizeProviderMetadata,
} from "./usage";

describe("sanitizeProviderMetadata", () => {
  test("allows only safe fields", () => {
    const metadata = {
      openrouter: {
        usage: { inputTokens: 10, outputTokens: 20, cost: 0.001 },
        cost: 0.001,
        model: "gpt-4",
        internal_id: "should-be-stripped",
        debug_info: "should-be-stripped",
      },
    };

    const result = sanitizeProviderMetadata(metadata);

    expect(result).toEqual({
      openrouter: {
        usage: { inputTokens: 10, outputTokens: 20, cost: 0.001 },
        cost: 0.001,
        model: "gpt-4",
      },
    });
  });

  test("strips sensitive fields", () => {
    const metadata = {
      provider: {
        api_key: "secret",
        user_id: "user_123",
        usage: { totalTokens: 100 },
      },
    };

    const result = sanitizeProviderMetadata(metadata);

    expect(result).toEqual({
      provider: {
        usage: { totalTokens: 100 },
      },
    });
  });

  test("handles nested objects", () => {
    const metadata = {
      openrouter: {
        usage: { inputTokens: 5, outputTokens: 10 },
        nested: { sensitive: "data" },
      },
    };

    const result = sanitizeProviderMetadata(metadata);

    expect(result).toEqual({
      openrouter: {
        usage: { inputTokens: 5, outputTokens: 10 },
      },
    });
  });

  test("returns undefined for empty input", () => {
    expect(sanitizeProviderMetadata(undefined)).toBeUndefined();
    expect(sanitizeProviderMetadata({})).toBeUndefined();
  });

  test("handles non-object provider data", () => {
    const metadata = {
      provider: "string-value",
      other: null,
    };

    const result = sanitizeProviderMetadata(metadata);

    expect(result).toBeUndefined();
  });
});

describe("calculateUsageStats", () => {
  test("sums message-level usage correctly", () => {
    const messages = [
      {
        metadata: {
          usage: { totalTokens: 1000, inputTokens: 500, outputTokens: 500 },
        },
      },
      {
        metadata: {
          usage: { totalTokens: 500, inputTokens: 200, outputTokens: 300 },
        },
      },
    ];

    const result = calculateUsageStats(messages);

    expect(result.totalTokens).toBe(1500);
    expect(result.inputTokens).toBe(700);
    expect(result.outputTokens).toBe(800);
  });

  test("handles missing metadata gracefully", () => {
    const messages = [
      { metadata: { usage: { totalTokens: 100 } } },
      { metadata: {} },
      { metadata: undefined },
      {},
    ];

    const result = calculateUsageStats(messages);

    expect(result.totalTokens).toBe(100);
  });

  test("returns empty stats for empty messages", () => {
    const result = calculateUsageStats([]);

    expect(result).toEqual(emptyUsageStats());
  });
});

describe("addUsage", () => {
  test("adds usage fields correctly", () => {
    const acc = emptyUsageStats();
    const step = {
      inputTokens: 100,
      outputTokens: 200,
      reasoningTokens: 50,
      totalTokens: 350,
    };

    const result = addUsage(acc, step);

    expect(result.inputTokens).toBe(100);
    expect(result.outputTokens).toBe(200);
    expect(result.reasoningTokens).toBe(50);
    expect(result.totalTokens).toBe(350);
  });

  test("handles undefined fields", () => {
    const acc = { ...emptyUsageStats(), inputTokens: 10 };
    const step = { outputTokens: 20 };

    const result = addUsage(acc, step);

    expect(result.inputTokens).toBe(10);
    expect(result.outputTokens).toBe(20);
  });

  test("returns accumulated when step is null", () => {
    const acc = { ...emptyUsageStats(), totalTokens: 100 };
    const result = addUsage(acc, null);
    expect(result).toBe(acc);
  });

  test("accumulates cache tokens from inputTokenDetails", () => {
    const acc = emptyUsageStats();
    const step = {
      inputTokens: 1000,
      outputTokens: 50,
      inputTokenDetails: {
        cacheReadTokens: 800,
        cacheWriteTokens: 100,
      },
    };
    const result = addUsage(acc, step);
    expect(result.cacheReadTokens).toBe(800);
    expect(result.cacheWriteTokens).toBe(100);
  });

  test("falls back to cachedInputTokens when inputTokenDetails is absent", () => {
    const acc = emptyUsageStats();
    const step = {
      inputTokens: 1000,
      outputTokens: 50,
      cachedInputTokens: 600,
    };
    const result = addUsage(acc, step);
    expect(result.cacheReadTokens).toBe(600);
    expect(result.cacheWriteTokens).toBe(0);
  });

  test("inputTokenDetails takes precedence over cachedInputTokens", () => {
    const acc = emptyUsageStats();
    const step = {
      inputTokens: 1000,
      cachedInputTokens: 999, // would be wrong
      inputTokenDetails: { cacheReadTokens: 800, cacheWriteTokens: 0 },
    };
    const result = addUsage(acc, step);
    expect(result.cacheReadTokens).toBe(800);
  });
});

describe("calculateUsageStats — cache fields", () => {
  test("sums cache read/write across messages", () => {
    const messages = [
      {
        metadata: {
          usage: {
            inputTokens: 100,
            outputTokens: 50,
            inputTokenDetails: { cacheReadTokens: 80, cacheWriteTokens: 10 },
          },
        },
      },
      {
        metadata: {
          usage: {
            inputTokens: 200,
            outputTokens: 25,
            inputTokenDetails: { cacheReadTokens: 150 },
          },
        },
      },
    ];
    const result = calculateUsageStats(messages);
    expect(result.cacheReadTokens).toBe(230);
    expect(result.cacheWriteTokens).toBe(10);
  });
});

describe("cacheHitRatio", () => {
  // The observed bug: dividing cache reads by the UNCACHED remainder reported
  // 5,039,266%. `inputTokens` is the whole prompt, so cache reads are a subset.
  test("is a share of the whole prompt, never above 100%", () => {
    expect(
      cacheHitRatio({
        ...emptyUsageStats(),
        inputTokens: 1_728_952,
        cacheReadTokens: 1_612_565,
        cacheWriteTokens: 116_355,
      }),
    ).toBeCloseTo(0.9327, 4);
  });

  test("caps a provider that reports cache outside inputTokens", () => {
    expect(
      cacheHitRatio({
        ...emptyUsageStats(),
        inputTokens: 32,
        cacheReadTokens: 1_612_565,
      }),
    ).toBe(1);
  });

  test("is zero with no prompt", () => {
    expect(cacheHitRatio(emptyUsageStats())).toBe(0);
  });
});
