import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { SpanContextService } from "./span-context.service";
import { context, trace, type Span, type Context } from "@opentelemetry/api";
import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks";

describe("SpanContextService", () => {
  let service: SpanContextService;
  let contextManager: AsyncHooksContextManager;

  beforeEach(() => {
    // Set up context manager for proper context propagation in tests
    contextManager = new AsyncHooksContextManager();
    contextManager.enable();
    context.setGlobalContextManager(contextManager);
    service = new SpanContextService();
  });

  afterEach(() => {
    // Clean up context manager
    contextManager.disable();
    context.disable();
  });

  describe("getActiveSpan", () => {
    it("should return undefined when no span is active", () => {
      const activeSpan = service.getActiveSpan();
      expect(activeSpan).toBeUndefined();
    });

    it("should return the active span when one exists", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      const result = context.with(trace.setSpan(context.active(), span), () => {
        return service.getActiveSpan();
      });

      expect(result).toBeDefined();
      expect(result).toBe(span);
      span.end();
    });
  });

  describe("getContext", () => {
    it("should return the current context", () => {
      const ctx = service.getContext();
      expect(ctx).toBeDefined();
    });

    it("should return the active context", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      const result = context.with(trace.setSpan(context.active(), span), () => {
        return service.getContext();
      });

      expect(result).toBeDefined();
      span.end();
    });
  });

  describe("with", () => {
    it("should execute function within the provided context", () => {
      const customContext = context.active();
      let executedInContext = false;

      const result = service.with(customContext, () => {
        executedInContext = true;
        return "test-result";
      });

      expect(executedInContext).toBe(true);
      expect(result).toBe("test-result");
    });

    it("should propagate the context to the function", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");
      const spanContext = trace.setSpan(context.active(), span);

      const result = service.with(spanContext, () => {
        const activeSpan = trace.getActiveSpan();
        return activeSpan;
      });

      expect(result).toBe(span);
      span.end();
    });

    it("should handle exceptions in the function", () => {
      const customContext = context.active();

      expect(() => {
        service.with(customContext, () => {
          throw new Error("Test error");
        });
      }).toThrow("Test error");
    });

    it("should return the function result", () => {
      const customContext = context.active();
      const result = service.with(customContext, () => {
        return { data: "test", count: 42 };
      });

      expect(result).toEqual({ data: "test", count: 42 });
    });
  });

  describe("withActiveSpan", () => {
    it("should set span as active for function execution", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      const result = service.withActiveSpan(span, () => {
        const activeSpan = trace.getActiveSpan();
        return activeSpan;
      });

      expect(result).toBe(span);
      span.end();
    });

    it("should return the function result", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      const result = service.withActiveSpan(span, () => {
        return "executed-with-span";
      });

      expect(result).toBe("executed-with-span");
      span.end();
    });

    it("should handle async operations", async () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      const result = await service.withActiveSpan(span, async () => {
        return new Promise((resolve) => {
          setTimeout(() => resolve("async-result"), 10);
        });
      });

      expect(result).toBe("async-result");
      span.end();
    });

    it("should propagate exceptions", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("test-span");

      expect(() => {
        service.withActiveSpan(span, () => {
          throw new Error("Span execution error");
        });
      }).toThrow("Span execution error");

      span.end();
    });

    it("should nest spans correctly", () => {
      const tracer = trace.getTracer("test-tracer");
      const parentSpan = tracer.startSpan("parent-span");
      const childSpan = tracer.startSpan("child-span");

      const result = service.withActiveSpan(parentSpan, () => {
        return service.withActiveSpan(childSpan, () => {
          const activeSpan = trace.getActiveSpan();
          return activeSpan;
        });
      });

      expect(result).toBe(childSpan);
      childSpan.end();
      parentSpan.end();
    });
  });

  describe("integration scenarios", () => {
    it("should support complex span context propagation", () => {
      const tracer = trace.getTracer("test-tracer");
      const span1 = tracer.startSpan("span-1");
      const span2 = tracer.startSpan("span-2");

      // Execute with span1 active
      const ctx1 = trace.setSpan(context.active(), span1);
      const result1 = service.with(ctx1, () => {
        return service.getActiveSpan();
      });

      // Execute with span2 active
      const ctx2 = trace.setSpan(context.active(), span2);
      const result2 = service.with(ctx2, () => {
        return service.getActiveSpan();
      });

      expect(result1).toBe(span1);
      expect(result2).toBe(span2);

      span1.end();
      span2.end();
    });

    it("should handle context isolation", () => {
      const tracer = trace.getTracer("test-tracer");
      const span = tracer.startSpan("isolated-span");

      // Execute with span active
      service.withActiveSpan(span, () => {
        const activeInside = service.getActiveSpan();
        expect(activeInside).toBe(span);
      });

      // Outside the context, span should not be active
      const activeOutside = service.getActiveSpan();
      expect(activeOutside).not.toBe(span);

      span.end();
    });
  });
});
