import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { context, trace } from "@opentelemetry/api";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OtelTracer } from "./otel-tracer";

// Register the async context manager once for the entire test file.
// This must be done before any OtelTracer is constructed.
const ctxManager = new AsyncLocalStorageContextManager();
ctxManager.enable();
context.setGlobalContextManager(ctxManager);

function setupProvider(): { exporter: InMemorySpanExporter; provider: BasicTracerProvider } {
  const exporter = new InMemorySpanExporter();
  const provider = new BasicTracerProvider({
    spanProcessors: [new SimpleSpanProcessor(exporter)],
  });
  trace.setGlobalTracerProvider(provider);
  return { exporter, provider };
}

describe("OtelTracer", () => {
  let exporter: InMemorySpanExporter;
  let provider: BasicTracerProvider;

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

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

  it("records span name and op attribute", async () => {
    const tracer = new OtelTracer();
    await tracer.startSpan({ name: "blog.getArticles", op: "use-case" }, async () => "value");
    const spans = exporter.getFinishedSpans();
    expect(spans).toHaveLength(1);
    const [span] = spans;
    expect(span!.name).toBe("blog.getArticles");
    expect(span!.attributes["span.op"]).toBe("use-case");
  });

  it("records additional attributes, filtering out null values", async () => {
    const tracer = new OtelTracer();
    await tracer.startSpan(
      {
        name: "articles.findAll",
        op: "repository",
        attributes: { collection: "articles", limit: 10, tag: null },
      },
      async () => undefined,
    );
    const [span] = exporter.getFinishedSpans();
    expect(span!.attributes["collection"]).toBe("articles");
    expect(span!.attributes["limit"]).toBe(10);
    expect(span!.attributes["tag"]).toBeUndefined();
  });

  it("nested spans — child span has parent span as its parent", async () => {
    const tracer = new OtelTracer();
    await tracer.startSpan({ name: "parent" }, async () => {
      await tracer.startSpan({ name: "child" }, async () => "child-result");
      return "parent-result";
    });
    const spans = exporter.getFinishedSpans();
    expect(spans).toHaveLength(2);
    const child = spans.find((s) => s.name === "child")!;
    const parent = spans.find((s) => s.name === "parent")!;
    // In sdk-trace-base@1.30.x the parent-child link is tracked via parentSpanId (string)
    // Both spans are in the same trace
    expect(child.spanContext().traceId).toBe(parent.spanContext().traceId);
    // The child's parentSpanId should be the parent's span ID
    expect(child.parentSpanId).toBe(parent.spanContext().spanId);
  });

  it("records exception and sets ERROR status on throw", async () => {
    const tracer = new OtelTracer();
    await expect(
      tracer.startSpan({ name: "failing-op" }, async () => {
        throw new Error("boom");
      }),
    ).rejects.toThrow("boom");

    const [span] = exporter.getFinishedSpans();
    expect(span!.status.code).toBe(2); // SpanStatusCode.ERROR = 2
    const exceptionEvent = span!.events.find((e) => e.name === "exception");
    expect(exceptionEvent).toBeDefined();
    expect(exceptionEvent!.attributes!["exception.message"]).toBe("boom");
  });

  it("ISpan adapter: setAttribute ignores null; setStatus maps ok/error", async () => {
    const tracer = new OtelTracer();
    await tracer.startSpan({ name: "adapter-test" }, async (span) => {
      span.setAttribute("key", "value");
      span.setAttribute("nullable", null);
      span.setStatus("ok");
      return undefined;
    });
    const [span] = exporter.getFinishedSpans();
    expect(span!.attributes["key"]).toBe("value");
    expect(span!.attributes["nullable"]).toBeUndefined();
    expect(span!.status.code).toBe(1); // SpanStatusCode.OK = 1
  });
});
