import { describe, it, expect, vi, beforeEach } from "vitest";
import { TelemetryInterceptor } from "./telemetry.interceptor";
import { TelemetryService } from "./telemetry.service";
import type { ExecutionContext, CallHandler } from "@nestjs/common";
import type { Span } from "@opentelemetry/api";
import { of, throwError } from "rxjs";
import { lastValueFrom } from "rxjs";

describe("TelemetryInterceptor", () => {
  let interceptor: TelemetryInterceptor;
  let telemetryService: TelemetryService;
  let mockSpan: Span;
  let mockContext: ExecutionContext;
  let mockHandler: CallHandler;

  beforeEach(() => {
    // Mock span
    mockSpan = {
      end: vi.fn(),
      setAttribute: vi.fn(),
      setAttributes: vi.fn(),
      addEvent: vi.fn(),
      setStatus: vi.fn(),
      updateName: vi.fn(),
      isRecording: vi.fn().mockReturnValue(true),
      recordException: vi.fn(),
      spanContext: vi.fn().mockReturnValue({
        traceId: "test-trace-id",
        spanId: "test-span-id",
      }),
    } as unknown as Span;

    // Mock telemetry service
    telemetryService = {
      startSpan: vi.fn().mockReturnValue(mockSpan),
      recordException: vi.fn(),
      getTracer: vi.fn(),
      onModuleInit: vi.fn(),
      onModuleDestroy: vi.fn(),
    } as unknown as TelemetryService;

    // Mock execution context
    mockContext = {
      switchToHttp: vi.fn().mockReturnValue({
        getRequest: vi.fn().mockReturnValue({
          method: "GET",
          url: "/api/test",
          path: "/api/test",
        }),
        getResponse: vi.fn().mockReturnValue({
          statusCode: 200,
          setHeader: vi.fn(),
          headersSent: false,
          writableEnded: false,
        }),
      }),
      getClass: vi.fn().mockReturnValue({ name: "TestController" }),
      getHandler: vi.fn().mockReturnValue({ name: "testHandler" }),
    } as unknown as ExecutionContext;

    interceptor = new TelemetryInterceptor(telemetryService);
  });

  describe("intercept", () => {
    it("should create a span for HTTP request", async () => {
      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "test" })),
      } as unknown as CallHandler;

      await lastValueFrom(interceptor.intercept(mockContext, mockHandler));

      expect(telemetryService.startSpan).toHaveBeenCalledWith(
        "GET /api/test",
        expect.objectContaining({
          attributes: expect.objectContaining({
            "http.request.method": "GET",
            "url.path": "/api/test",
          }),
        })
      );
    });

    it("should set http.status_code attribute on success", async () => {
      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "test" })),
      } as unknown as CallHandler;

      await lastValueFrom(interceptor.intercept(mockContext, mockHandler));

      expect(mockSpan.setAttribute).toHaveBeenCalledWith("http.response.status_code", 200);
      expect(mockSpan.end).toHaveBeenCalled();
    });

    it("should add trace context to response headers", async () => {
      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "test" })),
      } as unknown as CallHandler;

      const mockResponse = mockContext.switchToHttp().getResponse();

      await lastValueFrom(interceptor.intercept(mockContext, mockHandler));

      expect(mockResponse.setHeader).toHaveBeenCalledWith("x-trace-id", "test-trace-id");
    });

    it("should not set trace header when response is already committed", async () => {
      const committedResponseContext = {
        ...mockContext,
        switchToHttp: vi.fn().mockReturnValue({
          getRequest: vi.fn().mockReturnValue({
            method: "GET",
            url: "/api/test",
            path: "/api/test",
          }),
          getResponse: vi.fn().mockReturnValue({
            statusCode: 200,
            setHeader: vi.fn(),
            headersSent: true,
            writableEnded: true,
          }),
        }),
      } as unknown as ExecutionContext;

      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "test" })),
      } as unknown as CallHandler;

      const committedResponse = committedResponseContext.switchToHttp().getResponse();

      await lastValueFrom(interceptor.intercept(committedResponseContext, mockHandler));

      expect(committedResponse.setHeader).not.toHaveBeenCalled();
    });

    it("should record exception on error", async () => {
      const error = new Error("Test error");
      mockHandler = {
        handle: vi.fn().mockReturnValue(throwError(() => error)),
      } as unknown as CallHandler;

      await expect(lastValueFrom(interceptor.intercept(mockContext, mockHandler))).rejects.toThrow(
        "Test error"
      );

      expect(telemetryService.recordException).toHaveBeenCalledWith(mockSpan, error);
      expect(mockSpan.end).toHaveBeenCalled();
    });

    it("should end span even if error occurs", async () => {
      const error = new Error("Test error");
      mockHandler = {
        handle: vi.fn().mockReturnValue(throwError(() => error)),
      } as unknown as CallHandler;

      await expect(
        lastValueFrom(interceptor.intercept(mockContext, mockHandler))
      ).rejects.toThrow();

      expect(mockSpan.end).toHaveBeenCalled();
    });

    it("should handle different HTTP methods", async () => {
      const postContext = {
        ...mockContext,
        switchToHttp: vi.fn().mockReturnValue({
          getRequest: vi.fn().mockReturnValue({
            method: "POST",
            url: "/api/test",
            path: "/api/test",
          }),
          getResponse: vi.fn().mockReturnValue({
            statusCode: 201,
            setHeader: vi.fn(),
          }),
        }),
      } as unknown as ExecutionContext;

      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "created" })),
      } as unknown as CallHandler;

      await lastValueFrom(interceptor.intercept(postContext, mockHandler));

      expect(telemetryService.startSpan).toHaveBeenCalledWith(
        "POST /api/test",
        expect.objectContaining({
          attributes: expect.objectContaining({
            "http.request.method": "POST",
          }),
        })
      );
    });

    it("should set controller and handler attributes", async () => {
      mockHandler = {
        handle: vi.fn().mockReturnValue(of({ data: "test" })),
      } as unknown as CallHandler;

      await lastValueFrom(interceptor.intercept(mockContext, mockHandler));

      expect(telemetryService.startSpan).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          attributes: expect.objectContaining({
            "code.function": "testHandler",
            "code.namespace": "TestController",
          }),
        })
      );
    });
  });
});
