import { describe, it, expect, vi, beforeEach } from "vitest";
import type { ArgumentsHost } from "@nestjs/common";
import { HttpException } from "@nestjs/common";
import type { Request, Response } from "express";
import { z, ZodError } from "zod";
import { GlobalErrorFilter } from "./global-error.filter.js";
import {
  NotFoundError,
  ValidationError,
  InternalError,
} from "./application-error.js";

function createMockHost(
  req: Partial<Request>,
  res: Partial<Response>,
): ArgumentsHost {
  return {
    switchToHttp: () => ({
      getResponse: () => res,
      getRequest: () => req,
    }),
  } as unknown as ArgumentsHost;
}

function createMockResponse(): {
  response: Response;
  statusMock: ReturnType<typeof vi.fn>;
  jsonMock: ReturnType<typeof vi.fn>;
} {
  const statusMock = vi.fn().mockReturnThis();
  const jsonMock = vi.fn();
  return {
    response: { status: statusMock, json: jsonMock } as unknown as Response,
    statusMock,
    jsonMock,
  };
}

function createMockRequest(headers: Record<string, unknown> = {}): Request {
  return { headers } as unknown as Request;
}

describe("GlobalErrorFilter", () => {
  let filter: GlobalErrorFilter;

  beforeEach(() => {
    filter = new GlobalErrorFilter();
  });

  describe("ApplicationError branch", () => {
    it("should respond with ApplicationError statusCode and toJSON body", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(
        createMockRequest({ "x-request-id": "trace-abc" }),
        response,
      );
      const error = new NotFoundError("Chapter", "ch-1");

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(404);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.objectContaining({
            code: "CONTENT_NOT_FOUND",
            traceId: "trace-abc",
          }),
        }),
      );
      expect(error.traceId).toBe("trace-abc");
    });

    it("should handle ValidationError with statusCode 400", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(createMockRequest(), response);
      const error = new ValidationError("Invalid input");

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(400);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({ code: "CONTENT_VALIDATION_ERROR" }),
        }),
      );
    });
  });

  describe("ZodError branch", () => {
    it("should respond with statusCode 400 and CONTENT_VALIDATION_ERROR", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(
        createMockRequest({ "x-request-id": "trace-zod" }),
        response,
      );
      const parseResult = z.object({ x: z.string() }).safeParse({ x: 123 });
      const zodError = parseResult.error as ZodError;

      filter.catch(zodError, host);

      expect(statusMock).toHaveBeenCalledWith(400);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.objectContaining({
            code: "CONTENT_VALIDATION_ERROR",
            message: "Validation failed",
            traceId: "trace-zod",
            details: expect.any(Object),
          }),
        }),
      );
    });
  });

  describe("HttpException branch", () => {
    it("should handle HttpException with string response", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(createMockRequest(), response);
      const error = new HttpException("Forbidden resource", 403);

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(403);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({
            code: "HTTP_ERROR",
            message: "Forbidden resource",
          }),
        }),
      );
    });

    it("should handle HttpException with object response containing message string", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(createMockRequest(), response);
      const error = new HttpException({ message: "Custom error message" }, 422);

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(422);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({
            message: "Custom error message",
          }),
        }),
      );
    });

    it("should fall back to exception.message for object response without message", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(createMockRequest(), response);
      const error = new HttpException({ data: "value" }, 400);

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(400);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({
            code: "HTTP_ERROR",
            message: error.message,
          }),
        }),
      );
    });
  });

  describe("unknown error branch", () => {
    it("should respond with statusCode 500 and INTERNAL_ERROR", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(
        createMockRequest({ "x-request-id": "trace-unknown" }),
        response,
      );
      const error = new Error("something exploded");

      filter.catch(error, host);

      expect(statusMock).toHaveBeenCalledWith(500);
      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.objectContaining({
            code: "INTERNAL_ERROR",
            message: "An unexpected error occurred",
            traceId: "trace-unknown",
          }),
        }),
      );
    });

    it("should default traceId to unknown when x-request-id header is missing", () => {
      const { response, statusMock, jsonMock } = createMockResponse();
      const host = createMockHost(createMockRequest(), response);
      const error = new InternalError("boom");

      filter.catch(error, host);

      expect(jsonMock).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.objectContaining({ traceId: "unknown" }),
        }),
      );
      expect(statusMock).toHaveBeenCalledWith(500);
    });
  });
});
