import { describe, it, expect, vi, beforeEach } from "vitest";
import { Request, Response } from "express";
import { ZodError, z } from "zod";
import { Prisma } from "@prisma/client";
import { errorMiddleware } from "./error.middleware";
import { sendSuccess, sendError } from "../utils/api-response";
import { BadRequestError } from "../errors/http-errors";

describe("Global Response and Error Handling", () => {
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;
  let nextFunction: any;

  beforeEach(() => {
    mockRequest = {
      path: "/test-route",
      originalUrl: "/test-route",
      headers: {
        "x-request-id": "test-req-id-123",
      },
    };

    mockResponse = {
      statusCode: 200,
      status: vi.fn().mockReturnThis(),
      json: vi.fn().mockReturnThis(),
      req: mockRequest as Request,
    } as unknown as Partial<Response>;

    nextFunction = vi.fn();
  });

  describe("ApiResponse Helpers", () => {
    it("should format sendSuccess response correctly", () => {
      sendSuccess(
        mockResponse as Response,
        { user: "Alice" },
        200,
        "Success Message",
      );

      expect(mockResponse.status).toHaveBeenCalledWith(200);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: true,
          statusCode: 200,
          message: "Success Message",
          data: { user: "Alice" },
          path: "/test-route",
          requestId: "test-req-id-123",
          timestamp: expect.any(String),
        }),
      );
    });

    it("should format sendError response correctly", () => {
      sendError(
        mockResponse as Response,
        "Custom error message",
        400,
        "TEST_ERROR",
        { field: "email" },
      );

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 400,
          error: "Custom error message",
          errorCode: "TEST_ERROR",
          details: { field: "email" },
          path: "/test-route",
          requestId: "test-req-id-123",
          timestamp: expect.any(String),
        }),
      );
    });
  });

  describe("Error Middleware Mapping", () => {
    it("should map custom AppError (e.g. BadRequestError)", () => {
      const err = new BadRequestError(
        "Invalid input parameter",
        "CUSTOM_BAD_REQUEST",
        { param: "limit" },
      );

      errorMiddleware(
        err,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 400,
          error: "Invalid input parameter",
          errorCode: "CUSTOM_BAD_REQUEST",
          details: { param: "limit" },
        }),
      );
    });

    it("should map ZodError to 400 VALIDATION_ERROR", () => {
      const result = z.object({ email: z.string() }).safeParse({ email: 123 });
      expect(result.success).toBe(false);
      const zodError = (result as any).error;

      errorMiddleware(
        zodError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 400,
          errorCode: "VALIDATION_ERROR",
          error: "Validation failed",
        }),
      );
      const lastCallJson = (mockResponse.json as any).mock.calls[0][0];
      expect(lastCallJson.details.fieldErrors).toHaveProperty("email");
    });

    it("should strip body/query/params prefix for nested ZodErrors", () => {
      const schema = z.object({
        body: z.object({
          email: z.string(),
        }),
      });
      const result = schema.safeParse({ body: { email: 123 } });
      expect(result.success).toBe(false);
      const zodError = (result as any).error;

      errorMiddleware(
        zodError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      const lastCallJson = (mockResponse.json as any).mock.calls[0][0];
      expect(lastCallJson.details.fieldErrors).toHaveProperty("email");
      expect(lastCallJson.details.fieldErrors).not.toHaveProperty("body.email");
      expect(lastCallJson.details.fieldErrors).not.toHaveProperty("body");
    });

    it("should map Prisma P2002 Unique Constraint error to 409 CONFLICT", () => {
      const prismaError = new Prisma.PrismaClientKnownRequestError(
        "Unique constraint failed",
        {
          code: "P2002",
          clientVersion: "5.0.0",
          meta: { target: ["email"] },
        },
      );

      errorMiddleware(
        prismaError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(409);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 409,
          errorCode: "CONFLICT",
          error: expect.stringContaining("already exists"),
          details: { target: ["email"] },
        }),
      );
    });

    it("should map Prisma P2025 Record Not Found error to 404 NOT_FOUND", () => {
      const prismaError = new Prisma.PrismaClientKnownRequestError(
        "Record not found",
        {
          code: "P2025",
          clientVersion: "5.0.0",
        },
      );

      errorMiddleware(
        prismaError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(404);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 404,
          errorCode: "NOT_FOUND",
          error: "Requested resource not found.",
        }),
      );
    });

    it("should map JSON body parsing SyntaxError to 400 MALFORMED_JSON", () => {
      const syntaxError = new SyntaxError(
        "Unexpected token } in JSON at position 12",
      );
      (syntaxError as any).body = '{ "invalid" }';

      errorMiddleware(
        syntaxError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 400,
          errorCode: "MALFORMED_JSON",
          error: "Malformed JSON payload in request body.",
        }),
      );
    });

    it("should map JWT TokenExpiredError to 401 TOKEN_EXPIRED", () => {
      const jwtError = {
        name: "TokenExpiredError",
        message: "jwt expired",
      };

      errorMiddleware(
        jwtError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(401);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 401,
          errorCode: "TOKEN_EXPIRED",
          error: "Session token has expired.",
        }),
      );
    });

    it("should fallback to 500 INTERNAL_SERVER_ERROR for unhandled exceptions", () => {
      const unknownError = new Error("Database disconnected unexpectedly");

      errorMiddleware(
        unknownError,
        mockRequest as Request,
        mockResponse as Response,
        nextFunction,
      );

      expect(mockResponse.status).toHaveBeenCalledWith(500);
      expect(mockResponse.json).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          statusCode: 500,
          errorCode: "INTERNAL_SERVER_ERROR",
        }),
      );
    });
  });
});
