import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest, NextResponse } from "next/server";
import { ZodError, type ZodIssue } from "zod";
import {
  withError,
  withAuth,
  withAdmin,
  withEmailAccount,
  withEmailProvider,
  type RequestWithAuth,
  type NextHandler,
} from "./middleware";
import { EMAIL_ACCOUNT_HEADER } from "@/utils/config";
import prisma from "@/utils/__mocks__/prisma";
import { auth } from "@/utils/auth";
import { isAdmin } from "@/utils/admin";
import { getEmailAccount } from "@/utils/redis/account-validation";
import { captureException, checkCommonErrors, SafeError } from "@/utils/error";
import { createEmailProvider } from "@/utils/email/provider";
import { isProviderRateLimitModeError } from "@/utils/email/rate-limit-mode-error";
import { recordRateLimitFromApiError } from "@/utils/email/rate-limit";
import { getAuditContext } from "@/utils/audit/context";

vi.mock("better-auth", () => {
  const mockAuthFn = vi.fn();
  return {
    betterAuth: vi.fn(() => ({
      api: { getSession: mockAuthFn },
      signIn: vi.fn(),
      signOut: vi.fn(),
    })),
  };
});

vi.mock("@/utils/auth", () => ({
  auth: vi.fn(),
}));

vi.mock("@/utils/redis/account-validation");
vi.mock("@/utils/prisma");
vi.mock("@/utils/admin", () => ({
  isAdmin: vi.fn(),
}));
vi.mock("@/utils/email/provider", () => ({
  createEmailProvider: vi.fn(),
}));
vi.mock("@/utils/email/rate-limit", () => ({
  recordRateLimitFromApiError: vi.fn(),
}));
vi.mock("@/utils/email/rate-limit-mode-error", () => ({
  isProviderRateLimitModeError: vi.fn(),
}));

vi.mock("@/utils/error", async (importActual) => {
  const actual = await importActual<typeof import("@/utils/error")>();
  return {
    ...actual,
    captureException: vi.fn(),
    checkCommonErrors: vi.fn(),
  };
});

vi.mock("@/utils/error.server");

const mockAuth = vi.mocked(auth);
const mockGetEmailAccount = vi.mocked(getEmailAccount);
const mockCheckCommonErrors = vi.mocked(checkCommonErrors);
const mockCaptureException = vi.mocked(captureException);
const mockIsAdmin = vi.mocked(isAdmin);
const mockCreateEmailProvider = vi.mocked(createEmailProvider);
const mockPrismaEmailAccountFindUnique = vi.mocked(
  prisma.emailAccount.findUnique,
);
const mockIsProviderRateLimitModeError = vi.mocked(
  isProviderRateLimitModeError,
);
const mockRecordRateLimitFromApiError = vi.mocked(recordRateLimitFromApiError);

type RequestWithAuthAndEmail = RequestWithAuth & {
  auth: { emailAccountId: string; email: string };
};

describe("Middleware", () => {
  let mockReq: NextRequest;
  const mockContext = { params: Promise.resolve({}) };

  beforeEach(() => {
    vi.resetAllMocks();
    mockReq = createMockRequest();
  });

  describe("withError", () => {
    it("uses valid UUID v4 request IDs from the request header", async () => {
      const requestId = "123e4567-e89b-42d3-a456-426614174000";
      mockReq = createMockRequest("GET", "http://localhost/test", {
        "x-request-id": requestId,
      });
      const handler = vi.fn(async () =>
        NextResponse.json({ requestId: getAuditContext().requestId }),
      );
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(responseBody.requestId).toBe(requestId);
    });

    it("generates a fresh request ID for invalid request ID headers", async () => {
      mockReq = createMockRequest("GET", "http://localhost/test", {
        "x-request-id": "not-a-valid-request-id",
      });
      const handler = vi.fn(async () =>
        NextResponse.json({ requestId: getAuditContext().requestId }),
      );
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(responseBody.requestId).not.toBe("not-a-valid-request-id");
      expect(responseBody.requestId).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
      );
    });

    it("should call the handler and return its response on success", async () => {
      const mockResponse = NextResponse.json({ success: true });
      const handler = vi.fn().mockResolvedValue(mockResponse);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(handler).toHaveBeenCalledWith(mockReq, mockContext);
      expect(response.status).toBe(200);
      expect(responseBody).toEqual({ success: true });
    });

    it("should return 400 for ZodError", async () => {
      const zodError = new ZodError([
        { path: ["field"], message: "Required" },
      ] as ZodIssue[]);
      const handler = vi.fn().mockRejectedValue(zodError);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(response.status).toBe(400);
      expect(responseBody).toEqual({
        error: { issues: zodError.issues },
        isKnownError: true,
      });
    });

    it("should return 400 for SafeError", async () => {
      const safeError = new SafeError("User-friendly message");
      const handler = vi.fn().mockRejectedValue(safeError);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(response.status).toBe(400);
      expect(responseBody).toEqual({
        error: "User-friendly message",
        isKnownError: true,
      });
    });

    it("should respect SafeError status codes", async () => {
      const safeError = new SafeError("Slow down", 429);
      const handler = vi.fn().mockRejectedValue(safeError);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(response.status).toBe(429);
      expect(responseBody).toEqual({
        error: "Slow down",
        isKnownError: true,
      });
    });

    it("should ignore non-error SafeError status codes", async () => {
      const safeError = new SafeError("User-friendly message", 200);
      const handler = vi.fn().mockRejectedValue(safeError);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(response.status).toBe(400);
      expect(responseBody).toEqual({
        error: "User-friendly message",
        isKnownError: true,
      });
    });

    it("should handle common errors using checkCommonErrors", async () => {
      const commonError = { message: "API Error", code: 409, type: "Conflict" };
      mockCheckCommonErrors.mockReturnValue(commonError);
      const handler = vi.fn().mockRejectedValue(new Error("Some API error"));
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(checkCommonErrors).toHaveBeenCalled();
      expect(response.status).toBe(commonError.code);
      expect(responseBody).toEqual({
        error: commonError.message,
        isKnownError: true,
      });
    });

    it("should still return 429 for rate-limit API errors", async () => {
      const rateLimitError = new Error("Rate limit exceeded");
      const commonError = {
        message: "Gmail error: retry later",
        code: 429,
        type: "Gmail Rate Limit Exceeded",
      };
      mockCheckCommonErrors.mockReturnValue(commonError);
      mockRecordRateLimitFromApiError.mockResolvedValueOnce("google");
      (mockReq as any).auth = { emailAccountId: "acc-456" };

      const handler = vi.fn().mockRejectedValue(rateLimitError);
      const wrappedHandler = withError("labels", handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(mockRecordRateLimitFromApiError).toHaveBeenCalledWith(
        expect.objectContaining({
          apiErrorType: commonError.type,
          error: rateLimitError,
          emailAccountId: "acc-456",
        }),
      );
      expect(response.status).toBe(429);
      expect(responseBody).toEqual({
        error: commonError.message,
        isKnownError: true,
      });
    });

    it("should return 500 and capture unhandled errors", async () => {
      const unexpectedError = new Error("Something went very wrong");
      mockCheckCommonErrors.mockReturnValue(null);
      const handler = vi.fn().mockRejectedValue(unexpectedError);
      const wrappedHandler = withError(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(checkCommonErrors).toHaveBeenCalled();
      expect(mockCaptureException).toHaveBeenCalledWith(unexpectedError, {
        extra: { url: mockReq.url },
      });
      expect(response.status).toBe(500);
      expect(responseBody).toEqual({ error: "An unexpected error occurred" });
    });
  });

  describe("withAuth", () => {
    const mockUserId = "user-123";

    it("should call the handler with auth info if session exists", async () => {
      mockAuth.mockResolvedValue({ user: { id: mockUserId } } as any);
      const handler = vi.fn(async (_req: RequestWithAuth, _ctx: any) =>
        NextResponse.json({ ok: true }),
      );
      const wrappedHandler = withAuth(handler);

      await wrappedHandler(mockReq, mockContext);

      expect(auth).toHaveBeenCalledTimes(1);
      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          auth: { userId: mockUserId },
        }),
        mockContext,
      );
    });

    it("should return 401 if session does not exist", async () => {
      mockAuth.mockResolvedValue(null as any);
      const handler: NextHandler<RequestWithAuth> = vi.fn();
      const wrappedHandler = withAuth(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(auth).toHaveBeenCalledTimes(1);
      expect(handler).not.toHaveBeenCalled();
      expect(response.status).toBe(401);
      expect(responseBody).toEqual({
        error: "Unauthorized",
        isKnownError: true,
      });
    });

    it("should return 500 if auth throws", async () => {
      const authError = new Error("Session lookup failed");
      mockAuth.mockRejectedValue(authError);
      const handler: NextHandler<RequestWithAuth> = vi.fn();
      const wrappedHandler = withAuth(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(auth).toHaveBeenCalledTimes(1);
      expect(handler).not.toHaveBeenCalled();
      expect(mockCaptureException).toHaveBeenCalledWith(authError, {
        extra: { url: mockReq.url },
      });
      expect(response.status).toBe(500);
      expect(responseBody).toEqual({
        error: "An unexpected error occurred",
      });
    });
  });

  describe("withAdmin", () => {
    const mockUserId = "user-123";

    it("should call the handler for admin users", async () => {
      mockAuth.mockResolvedValue({ user: { id: mockUserId } } as any);
      prisma.user.findUnique.mockResolvedValue({
        email: "admin@example.com",
      } as any);
      mockIsAdmin.mockReturnValue(true);

      const handler = vi.fn(async (_req: RequestWithAuth, _ctx: any) =>
        NextResponse.json({ ok: true }),
      );
      const wrappedHandler = withAdmin("admin/test", handler);

      await wrappedHandler(mockReq, mockContext);

      expect(auth).toHaveBeenCalledTimes(1);
      expect(prisma.user.findUnique).toHaveBeenCalledWith({
        where: { id: mockUserId },
        select: { email: true },
      });
      expect(mockIsAdmin).toHaveBeenCalledWith({ email: "admin@example.com" });
      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          auth: { userId: mockUserId },
        }),
        mockContext,
      );
    });

    it("should return 403 if user is not admin", async () => {
      mockAuth.mockResolvedValue({ user: { id: mockUserId } } as any);
      prisma.user.findUnique.mockResolvedValue({
        email: "user@example.com",
      } as any);
      mockIsAdmin.mockReturnValue(false);

      const handler: NextHandler<RequestWithAuth> = vi.fn();
      const wrappedHandler = withAdmin("admin/test", handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(handler).not.toHaveBeenCalled();
      expect(response.status).toBe(403);
      expect(responseBody).toEqual({
        error: "Unauthorized",
        isKnownError: true,
      });
    });

    it("should return 401 if session does not exist", async () => {
      mockAuth.mockResolvedValue(null as any);

      const handler: NextHandler<RequestWithAuth> = vi.fn();
      const wrappedHandler = withAdmin("admin/test", handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(prisma.user.findUnique).not.toHaveBeenCalled();
      expect(mockIsAdmin).not.toHaveBeenCalled();
      expect(handler).not.toHaveBeenCalled();
      expect(response.status).toBe(401);
      expect(responseBody).toEqual({
        error: "Unauthorized",
        isKnownError: true,
      });
    });
  });

  describe("withEmailAccount", () => {
    const mockUserId = "user-123";
    const mockAccountId = "acc-456";
    const mockEmail = "test@example.com";

    beforeEach(() => {
      mockAuth.mockResolvedValue({ user: { id: mockUserId } } as any);
    });

    it("should call handler with email account info if header exists and account is valid", async () => {
      mockReq = createMockRequest("GET", "http://localhost/api/test", {
        [EMAIL_ACCOUNT_HEADER]: mockAccountId,
      });
      mockGetEmailAccount.mockResolvedValue(mockEmail);

      const handler = vi.fn(async (_req: RequestWithAuthAndEmail, _ctx: any) =>
        NextResponse.json({ success: true }),
      );
      const wrappedHandler = withEmailAccount(handler);

      await wrappedHandler(mockReq, mockContext);

      expect(getEmailAccount).toHaveBeenCalledWith({
        userId: mockUserId,
        emailAccountId: mockAccountId,
      });
      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          auth: {
            userId: mockUserId,
            emailAccountId: mockAccountId,
            email: mockEmail,
          },
        }),
        mockContext,
      );
    });

    it("should return 403 if email account header is missing", async () => {
      const handler = createEmailAccountHandler();
      const wrappedHandler = withEmailAccount(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(auth).toHaveBeenCalledTimes(1);
      expect(getEmailAccount).not.toHaveBeenCalled();
      expect(handler).not.toHaveBeenCalled();
      expect(response.status).toBe(403);
      expect(responseBody).toEqual({
        error: "Email account ID is required",
        isKnownError: true,
      });
    });

    it("should return 403 if email account ID is invalid", async () => {
      mockReq = createMockRequest("GET", "http://localhost/api/test", {
        [EMAIL_ACCOUNT_HEADER]: mockAccountId,
      });
      mockGetEmailAccount.mockResolvedValue(null);

      const handler = createEmailAccountHandler();
      const wrappedHandler = withEmailAccount(handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(auth).toHaveBeenCalledTimes(1);
      expect(getEmailAccount).toHaveBeenCalledWith({
        userId: mockUserId,
        emailAccountId: mockAccountId,
      });
      expect(handler).not.toHaveBeenCalled();
      expect(response.status).toBe(403);
      expect(responseBody).toEqual({
        error: "Invalid account ID",
        isKnownError: true,
      });
    });
  });

  describe("withEmailProvider", () => {
    const mockUserId = "user-123";
    const mockAccountId = "acc-456";
    const mockEmail = "test@example.com";

    beforeEach(() => {
      mockAuth.mockResolvedValue({ user: { id: mockUserId } } as any);
    });

    it.each([
      [
        "Gmail",
        "google",
        {
          type: "Gmail Rate Limit Exceeded",
          message: "Gmail error: retry later",
          code: 429,
        },
      ],
      [
        "Outlook",
        "microsoft",
        {
          type: "Outlook Rate Limit",
          message: "Microsoft is temporarily limiting requests.",
          code: 429,
        },
      ],
    ] as const)("should return 429 for %s rate-limit mode errors from provider initialization", async (_caseName, provider, commonError) => {
      mockReq = createMockRequest("GET", "http://localhost/api/labels", {
        [EMAIL_ACCOUNT_HEADER]: mockAccountId,
      });
      mockGetEmailAccount.mockResolvedValue(mockEmail);
      mockPrismaEmailAccountFindUnique.mockResolvedValue({
        id: mockAccountId,
        account: { provider },
      } as any);

      const rateLimitError = new Error("Rate-limit mode active");
      mockCreateEmailProvider.mockRejectedValue(rateLimitError);
      mockIsProviderRateLimitModeError.mockImplementation(
        (error) => error === rateLimitError,
      );
      mockCheckCommonErrors.mockReturnValue(commonError);

      const handler = vi.fn(async () => NextResponse.json({ ok: true }));
      const wrappedHandler = withEmailProvider("labels", handler);

      const response = await wrappedHandler(mockReq, mockContext);
      const responseBody = await response.json();

      expect(handler).not.toHaveBeenCalled();
      expect(checkCommonErrors).toHaveBeenCalledWith(
        rateLimitError,
        mockReq.url,
        expect.anything(),
      );
      expect(mockRecordRateLimitFromApiError).toHaveBeenCalledWith(
        expect.objectContaining({
          apiErrorType: commonError.type,
          error: rateLimitError,
          emailAccountId: mockAccountId,
          source: "labels",
        }),
      );
      expect(response.status).toBe(429);
      expect(responseBody).toEqual({
        error: commonError.message,
        isKnownError: true,
      });
    });
  });
});

function createMockRequest(
  method = "GET",
  url = "http://localhost/test",
  headers?: Record<string, string>,
): NextRequest {
  const request = new NextRequest(url, {
    method,
    headers: new Headers(headers),
  });
  request.clone = vi.fn(() => request) as any;
  return request;
}

function createEmailAccountHandler() {
  return vi.fn(
    async (
      _req: RequestWithAuthAndEmail,
      _ctx: { params: Promise<Record<string, string>> },
    ): Promise<NextResponse> => NextResponse.json({}),
  );
}
