import { describe, it, expect, beforeAll, afterAll } from "vitest";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { prisma } from "../../config/prisma";
import * as authService from "./auth.service";
import { env } from "../../config/env";

describe("AuthService Integration Tests", () => {
  const testEmail = "test-auth-user@example.com";
  const testPassword = "Password123!";
  const testName = "Test Auth User";

  // Clean up helper
  const cleanDb = async () => {
    // Delete test users and related database entries
    const testUsers = await prisma.user.findMany({
      where: {
        email: {
          in: [testEmail, "test-auth-forgot@example.com"],
        },
      },
    });
    const userIds = testUsers.map((u) => u.id);

    if (userIds.length > 0) {
      await prisma.session.deleteMany({
        where: { userId: { in: userIds } },
      });
      await prisma.user.deleteMany({
        where: { id: { in: userIds } },
      });
    }

    await prisma.otpVerification.deleteMany({
      where: {
        email: {
          in: [testEmail, "test-auth-forgot@example.com"],
        },
      },
    });
  };

  beforeAll(async () => {
    await cleanDb();
  });

  afterAll(async () => {
    await cleanDb();
  });

  describe("register()", () => {
    it("should successfully register a new user (inactive/unverified), generate OTP, and verify OTP to activate and return tokens", async () => {
      const registerResult = await authService.register({
        name: testName,
        email: testEmail,
        password: testPassword,
      });

      expect(registerResult).toHaveProperty("message");
      expect(registerResult.email).toBe(testEmail);

      // Verify user is in DB as inactive
      const userBeforeVerify = await prisma.user.findUnique({
        where: { email: testEmail },
      });
      expect(userBeforeVerify).not.toBeNull();
      expect(userBeforeVerify?.name).toBe(testName);
      expect(userBeforeVerify?.status).toBe("INACTIVE");

      // Verify password was hashed
      const isPasswordHashed = await bcrypt.compare(
        testPassword,
        userBeforeVerify!.passwordHash,
      );
      expect(isPasswordHashed).toBe(true);

      // Get OTP verification code from DB
      const otpRecord = await prisma.otpVerification.findFirst({
        where: { email: testEmail, purpose: "EMAIL_VERIFICATION" },
      });
      expect(otpRecord).not.toBeNull();
      expect(otpRecord?.otp).toHaveLength(6);

      // Verify the OTP to activate user and obtain tokens
      const verifyResult = await authService.verifyOtp(
        testEmail,
        otpRecord!.otp,
        "EMAIL_VERIFICATION",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "127.0.0.1",
      );

      expect(verifyResult.verified).toBe(true);
      expect(verifyResult).toHaveProperty("accessToken");
      expect(verifyResult).toHaveProperty("refreshToken");
      expect((verifyResult as any).user.email).toBe(testEmail);

      // Verify user is active in DB now
      const userAfterVerify = await prisma.user.findUnique({
        where: { email: testEmail },
      });
      expect(userAfterVerify?.status).toBe("ACTIVE");

      // Verify session was created
      const session = await prisma.session.findUnique({
        where: { refreshToken: (verifyResult as any).refreshToken },
      });
      expect(session).not.toBeNull();
      expect(session?.userId).toBe(userAfterVerify?.id);
    });

    it("should throw ConflictError if user tries to register with existing email", async () => {
      await expect(
        authService.register({
          name: "Another User",
          email: testEmail,
          password: "Password123!",
        }),
      ).rejects.toThrow("Email already registered");
    });
  });

  describe("login()", () => {
    it("should successfully login and record a new session", async () => {
      const result = await authService.login(
        {
          email: testEmail,
          password: testPassword,
        },
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "127.0.0.1",
      );

      expect(result).toHaveProperty("accessToken");
      expect(result).toHaveProperty("refreshToken");
      expect(result.user.email).toBe(testEmail);

      // Verify session exists in DB
      const session = await prisma.session.findUnique({
        where: { refreshToken: result.refreshToken },
      });
      expect(session).not.toBeNull();
    });

    it("should throw UnauthorizedError with invalid password", async () => {
      await expect(
        authService.login({
          email: testEmail,
          password: "WrongPassword!",
        }),
      ).rejects.toThrow("Invalid credentials");
    });
  });

  describe("refresh()", () => {
    it("should rotate refresh token and delete old session", async () => {
      // 1. Create a session via login
      const loginRes = await authService.login({
        email: testEmail,
        password: testPassword,
      });

      const oldRefreshToken = loginRes.refreshToken;

      // 2. Perform refresh
      const refreshRes = await authService.refresh(
        oldRefreshToken,
        "PostmanRuntime/7.29.2",
        "10.0.0.1",
      );

      expect(refreshRes).toHaveProperty("accessToken");
      expect(refreshRes).toHaveProperty("refreshToken");

      // 3. Verify old session is deleted
      const oldSession = await prisma.session.findUnique({
        where: { refreshToken: oldRefreshToken },
      });
      expect(oldSession).toBeNull();

      // 4. Verify new session exists
      const newSession = await prisma.session.findUnique({
        where: { refreshToken: refreshRes.refreshToken },
      });
      expect(newSession).not.toBeNull();
      expect(newSession?.userAgent).toBe("PostmanRuntime/7.29.2");
    });

    it("should throw UnauthorizedError for revoked/non-existent refresh token", async () => {
      await expect(
        authService.refresh("some-invalid-token-signature-or-uuid"),
      ).rejects.toThrow("Session expired or revoked");
    });
  });

  describe("Session Management", () => {
    it("should list active sessions and allow remote revocation", async () => {
      const user = await prisma.user.findUnique({
        where: { email: testEmail },
      });
      const userId = user!.id;

      // 1. Get active sessions
      const sessions = await authService.getSessions(userId);
      expect(sessions.length).toBeGreaterThan(0);

      const sessionToRevoke = sessions[0];

      // 2. Revoke the session
      await authService.revokeSession(userId, sessionToRevoke.id);

      // 3. Verify revoked session is gone
      const revokedSession = await prisma.session.findUnique({
        where: { id: sessionToRevoke.id },
      });
      expect(revokedSession).toBeNull();
    });
  });

  describe("Forgot Password & OTP Flow", () => {
    const forgotEmail = "test-auth-forgot@example.com";

    beforeAll(async () => {
      // Create user for forgot password
      const passwordHash = await bcrypt.hash("InitialPass123!", 12);
      await prisma.user.create({
        data: {
          name: "Forgot User",
          email: forgotEmail,
          passwordHash: passwordHash,
          role: "CLIENT",
          status: "ACTIVE",
        },
      });
    });

    it("should generate OTP and save verification record", async () => {
      const result = await authService.forgotPassword(forgotEmail);
      expect(result.message).toBe(
        "If an account with this email exists, a password reset OTP has been sent.",
      );

      const verification = await prisma.otpVerification.findFirst({
        where: { email: forgotEmail, purpose: "FORGOT_PASSWORD" },
        orderBy: { createdAt: "desc" },
      });

      expect(verification).not.toBeNull();
      expect(verification?.otp).toHaveLength(6);
      expect(verification?.verified).toBe(false);
    });

    it("should increment attempts on invalid OTP verify, and verify successfully on correct OTP", async () => {
      const verification = await prisma.otpVerification.findFirst({
        where: { email: forgotEmail, purpose: "FORGOT_PASSWORD" },
        orderBy: { createdAt: "desc" },
      });

      const correctOtp = verification!.otp;

      // 1. Attempt with incorrect OTP
      await expect(
        authService.verifyOtp(forgotEmail, "999999", "FORGOT_PASSWORD"),
      ).rejects.toThrow("Invalid OTP");

      // Verify attempts incremented
      const updatedVerify = await prisma.otpVerification.findUnique({
        where: { id: verification!.id },
      });
      expect(updatedVerify?.attempts).toBe(1);

      // 2. Attempt with correct OTP
      const verifyResult = await authService.verifyOtp(
        forgotEmail,
        correctOtp,
        "FORGOT_PASSWORD",
      );
      expect(verifyResult.verified).toBe(true);

      const finalVerify = await prisma.otpVerification.findUnique({
        where: { id: verification!.id },
      });
      expect(finalVerify?.verified).toBe(true);
    });

    it("should reset password with verified OTP and invalidate all sessions", async () => {
      const newPassword = "NewSecretPassword567!";

      // Create a dummy session for forgot user to verify invalidation
      const forgotUser = await prisma.user.findUnique({
        where: { email: forgotEmail },
      });
      await prisma.session.create({
        data: {
          userId: forgotUser!.id,
          refreshToken: "dummy-forgot-session-token",
          expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
        },
      });

      // Reset password
      const resetResult = await authService.resetPassword({
        email: forgotEmail,
        otp: "any-dummy-otp-since-already-verified-in-db",
        newPassword,
      });

      expect(resetResult.message).toBe("Password reset successfully");

      // Verify password changed
      const updatedUser = await prisma.user.findUnique({
        where: { id: forgotUser!.id },
      });
      const isNewPasswordCorrect = await bcrypt.compare(
        newPassword,
        updatedUser!.passwordHash,
      );
      expect(isNewPasswordCorrect).toBe(true);

      // Verify all sessions were deleted
      const sessionsCount = await prisma.session.count({
        where: { userId: forgotUser!.id },
      });
      expect(sessionsCount).toBe(0);

      // Verify OTP verification records deleted/cleaned up
      const otpCount = await prisma.otpVerification.count({
        where: { email: forgotEmail, purpose: "FORGOT_PASSWORD" },
      });
      expect(otpCount).toBe(0);
    });
  });
});
