import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { metrics } from "@opentelemetry/api";
import {
  InMemoryMetricExporter,
  MeterProvider,
  PeriodicExportingMetricReader,
  AggregationTemporality,
} from "@opentelemetry/sdk-metrics";
import { OtelMetrics } from "./otel-metrics";

function setupMeterProvider(): {
  exporter: InMemoryMetricExporter;
  provider: MeterProvider;
} {
  const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
  const reader = new PeriodicExportingMetricReader({
    exporter,
    exportIntervalMillis: 100,
  });
  const provider = new MeterProvider({ readers: [reader] });
  metrics.setGlobalMeterProvider(provider);
  return { exporter, provider };
}

describe("OtelMetrics", () => {
  let exporter: InMemoryMetricExporter;
  let provider: MeterProvider;

  beforeEach(() => {
    ({ exporter, provider } = setupMeterProvider());
  });

  afterEach(async () => {
    await provider.shutdown();
    metrics.disable();
  });

  it("counter() records a counter measurement", async () => {
    const otelMetrics = new OtelMetrics();
    otelMetrics.counter("http.requests", 1, { method: "GET" });

    await provider.forceFlush();
    const metrics_ = exporter.getMetrics();
    const counterMetric = metrics_
      .flatMap((rm) => rm.scopeMetrics)
      .flatMap((sm) => sm.metrics)
      .find((m) => m.descriptor.name === "http.requests");

    expect(counterMetric).toBeDefined();
    expect(counterMetric!.dataPoints).toHaveLength(1);
    expect(counterMetric!.dataPoints[0]!.value).toBe(1);
  });

  it("counter() defaults value to 1 when omitted", async () => {
    const otelMetrics = new OtelMetrics();
    otelMetrics.counter("events.processed");

    await provider.forceFlush();
    const metrics_ = exporter.getMetrics();
    const counterMetric = metrics_
      .flatMap((rm) => rm.scopeMetrics)
      .flatMap((sm) => sm.metrics)
      .find((m) => m.descriptor.name === "events.processed");

    expect(counterMetric).toBeDefined();
    expect(counterMetric!.dataPoints[0]!.value).toBe(1);
  });

  it("histogram() records a histogram measurement", async () => {
    const otelMetrics = new OtelMetrics();
    otelMetrics.histogram("http.duration", 250, { route: "/api/me" });

    await provider.forceFlush();
    const metrics_ = exporter.getMetrics();
    const histogramMetric = metrics_
      .flatMap((rm) => rm.scopeMetrics)
      .flatMap((sm) => sm.metrics)
      .find((m) => m.descriptor.name === "http.duration");

    expect(histogramMetric).toBeDefined();
    expect(histogramMetric!.dataPoints).toHaveLength(1);
    // Histogram data points carry a Histogram aggregate value with sum/count/buckets.
    const dp = histogramMetric!.dataPoints[0] as {
      value: { sum?: number; count: number };
    };
    expect(dp.value.sum).toBe(250);
    expect(dp.value.count).toBe(1);
  });

  it("gauge() records via UpDownCounter", async () => {
    const otelMetrics = new OtelMetrics();
    otelMetrics.gauge("queue.depth", 5, { queue: "emails" });

    await provider.forceFlush();
    const metrics_ = exporter.getMetrics();
    const gaugeMetric = metrics_
      .flatMap((rm) => rm.scopeMetrics)
      .flatMap((sm) => sm.metrics)
      .find((m) => m.descriptor.name === "queue.depth");

    expect(gaugeMetric).toBeDefined();
    expect(gaugeMetric!.dataPoints).toHaveLength(1);
  });

  it("lazily caches instrument instances — same counter object reused across calls", async () => {
    const otelMetrics = new OtelMetrics();
    otelMetrics.counter("reuse.test", 1);
    otelMetrics.counter("reuse.test", 2);

    await provider.forceFlush();
    const metrics_ = exporter.getMetrics();
    const counterMetric = metrics_
      .flatMap((rm) => rm.scopeMetrics)
      .flatMap((sm) => sm.metrics)
      .find((m) => m.descriptor.name === "reuse.test");

    // Cumulative: should accumulate both adds (1+2=3)
    expect(counterMetric!.dataPoints[0]!.value).toBe(3);
  });
});
