import { describe, it, expect, vi, beforeEach } from "vitest";
import { HttpException, HttpStatus } from "@nestjs/common";
import { GlobalExceptionFilter } from "./global-exception.filter";
import type { ArgumentsHost } from "@nestjs/common";

describe("GlobalExceptionFilter", () => {
  let filter: GlobalExceptionFilter;
  let mockJson: ReturnType<typeof vi.fn>;
  let mockStatus: ReturnType<typeof vi.fn>;
  let mockHost: ArgumentsHost;

  beforeEach(() => {
    filter = new GlobalExceptionFilter();
    mockJson = vi.fn();
    mockStatus = vi.fn().mockReturnValue({ json: mockJson });

    mockHost = {
      switchToHttp: vi.fn().mockReturnValue({
        getResponse: vi.fn().mockReturnValue({
          status: mockStatus,
        }),
        getRequest: vi.fn().mockReturnValue({
          method: "GET",
          url: "/test",
        }),
      }),
    } as unknown as ArgumentsHost;
  });

  describe("HttpException handling", () => {
    it("should return HttpException message for client errors", () => {
      const exception = new HttpException("Not Found", HttpStatus.NOT_FOUND);

      filter.catch(exception, mockHost);

      expect(mockStatus).toHaveBeenCalledWith(404);
      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          message: "Not Found",
          statusCode: 404,
        })
      );
    });

    it("should return generic message for 500 errors in production", () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "production";

      const exception = new HttpException(
        "Internal Server Error",
        HttpStatus.INTERNAL_SERVER_ERROR
      );

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
          statusCode: 500,
        })
      );

      process.env.NODE_ENV = originalEnv;
    });
  });

  describe("Error handling", () => {
    it("should return generic message for non-HttpException in production", () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "production";

      const exception = new Error("Database connection failed");

      filter.catch(exception, mockHost);

      expect(mockStatus).toHaveBeenCalledWith(500);
      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );

      process.env.NODE_ENV = originalEnv;
    });

    it("should return error message in development", () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = "development";

      const exception = new Error("Test error message");

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "Test error message",
        })
      );

      process.env.NODE_ENV = originalEnv;
    });
  });

  describe("Sensitive information redaction", () => {
    it("should redact messages containing password", () => {
      const exception = new HttpException("Invalid password format", HttpStatus.BAD_REQUEST);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should redact messages containing API key", () => {
      const exception = new HttpException("Invalid api_key provided", HttpStatus.UNAUTHORIZED);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should redact messages containing database errors", () => {
      const exception = new HttpException(
        "Database error: connection refused",
        HttpStatus.BAD_REQUEST
      );

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should redact messages containing file paths", () => {
      const exception = new HttpException(
        "File not found at /home/user/data",
        HttpStatus.NOT_FOUND
      );

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should redact messages containing IP addresses", () => {
      const exception = new HttpException(
        "Failed to connect to 192.168.1.1",
        HttpStatus.BAD_REQUEST
      );

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should redact messages containing Prisma errors", () => {
      const exception = new HttpException("Prisma query failed", HttpStatus.INTERNAL_SERVER_ERROR);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "An unexpected error occurred",
        })
      );
    });

    it("should allow safe error messages", () => {
      const exception = new HttpException("Resource not found", HttpStatus.NOT_FOUND);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          message: "Resource not found",
        })
      );
    });
  });

  describe("Response structure", () => {
    it("should include errorId in response", () => {
      const exception = new HttpException("Test error", HttpStatus.BAD_REQUEST);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          errorId: expect.stringMatching(/^[0-9a-f-]{36}$/),
        })
      );
    });

    it("should include timestamp in response", () => {
      const exception = new HttpException("Test error", HttpStatus.BAD_REQUEST);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/),
        })
      );
    });

    it("should include path in response", () => {
      const exception = new HttpException("Test error", HttpStatus.BAD_REQUEST);

      filter.catch(exception, mockHost);

      expect(mockJson).toHaveBeenCalledWith(
        expect.objectContaining({
          path: "/test",
        })
      );
    });
  });
});
