import { describe, it, expect, expectTypeOf, vi } from "vitest";
import { withSpan } from "@/instrumentation/with-span";
import type { ITracer, ISpan, SpanOpts } from "@/instrumentation/tracer.interface";
import type { Instrumented } from "@/conformance/brands";
import { isInstrumented } from "@/conformance/brand-runtime";

function makeRecordingTracer() {
  const calls: SpanOpts[] = [];
  const tracer: ITracer = {
    startSpan: vi.fn(async (opts, fn) => {
      calls.push(opts);
      const span: ISpan = { setAttribute: () => {}, setStatus: () => {} };
      return fn(span);
    }),
  };
  return { tracer, calls };
}

describe("withSpan", () => {
  it("wraps fn with a span using static opts", async () => {
    const { tracer, calls } = makeRecordingTracer();
    const fn = async (a: number, b: number) => a + b;
    const wrapped = withSpan(tracer, { name: "test.add", op: "use-case" }, fn);
    const result = await wrapped(2, 3);
    expect(result).toBe(5);
    expect(calls).toHaveLength(1);
    expect(calls[0]).toEqual({ name: "test.add", op: "use-case" });
  });

  it("wraps fn with span opts derived from args (function form)", async () => {
    const { tracer, calls } = makeRecordingTracer();
    const fn = async (id: string) => `result-${id}`;
    const wrapped = withSpan(
      tracer,
      ([id]) => ({ name: "test.byId", op: "repository", attributes: { id } }),
      fn,
    );
    const result = await wrapped("abc");
    expect(result).toBe("result-abc");
    expect(calls).toHaveLength(1);
    expect(calls[0]).toEqual({
      name: "test.byId",
      op: "repository",
      attributes: { id: "abc" },
    });
  });

  it("propagates errors thrown by fn", async () => {
    const { tracer } = makeRecordingTracer();
    const wrapped = withSpan(tracer, { name: "test.err" }, async () => {
      throw new Error("boom");
    });
    await expect(wrapped()).rejects.toThrow("boom");
  });

  it("preserves identity across multiple invocations (closure stable)", async () => {
    const { tracer, calls } = makeRecordingTracer();
    const wrapped = withSpan(tracer, { name: "test.same" }, async (n: number) => n);
    await wrapped(1);
    await wrapped(2);
    expect(calls).toHaveLength(2);
    expect(calls.every((c) => c.name === "test.same")).toBe(true);
  });
});

describe("withSpan — brand", () => {
  it("returns an Instrumented<F>", () => {
    const { tracer } = makeRecordingTracer();
    const fn = async (a: number) => a + 1;
    const wrapped = withSpan(tracer, { name: "test.brand", op: "use-case" }, fn);
    expectTypeOf(wrapped).toMatchTypeOf<Instrumented<typeof fn>>();
  });
});

describe("withSpan — runtime brand", () => {
  it("attaches __instrumented as a non-enumerable property on the wrapped function", async () => {
    const { tracer } = makeRecordingTracer();
    const fn = async (a: number) => a + 1;
    const wrapped = withSpan(tracer, { name: "test.brand", op: "use-case" }, fn);
    expect(isInstrumented(wrapped)).toBe(true);
    expect(Object.keys(wrapped)).not.toContain("__instrumented");
  });
});
