import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import {
  ConflictException,
  ForbiddenException,
  InternalServerErrorException,
  UnauthorizedException,
} from "@nestjs/common";
import { hash } from "bcryptjs";
import { LocalAuthService } from "./local-auth.service";
import { PrismaService } from "../../prisma/prisma.service";

describe("LocalAuthService", () => {
  let service: LocalAuthService;

  const mockTxSession = {
    create: vi.fn(),
  };

  const mockTxWorkspace = {
    findFirst: vi.fn(),
    create: vi.fn(),
  };

  const mockTxWorkspaceMember = {
    create: vi.fn(),
  };

  const mockTxUser = {
    create: vi.fn(),
    findUnique: vi.fn(),
  };

  const mockTx = {
    user: mockTxUser,
    workspace: mockTxWorkspace,
    workspaceMember: mockTxWorkspaceMember,
    session: mockTxSession,
  };

  const mockPrismaService = {
    user: {
      findUnique: vi.fn(),
    },
    session: {
      create: vi.fn(),
    },
    $transaction: vi
      .fn()
      .mockImplementation((fn: (tx: typeof mockTx) => Promise<unknown>) => fn(mockTx)),
  };

  const originalEnv = {
    BREAKGLASS_SETUP_TOKEN: process.env.BREAKGLASS_SETUP_TOKEN,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        LocalAuthService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
      ],
    }).compile();

    service = module.get<LocalAuthService>(LocalAuthService);
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    if (originalEnv.BREAKGLASS_SETUP_TOKEN !== undefined) {
      process.env.BREAKGLASS_SETUP_TOKEN = originalEnv.BREAKGLASS_SETUP_TOKEN;
    } else {
      delete process.env.BREAKGLASS_SETUP_TOKEN;
    }
  });

  describe("setup", () => {
    const validSetupArgs = {
      email: "admin@example.com",
      name: "Break Glass Admin",
      password: "securePassword123!",
      setupToken: "valid-token-123",
    };

    const mockCreatedUser = {
      id: "user-uuid-123",
      email: "admin@example.com",
      name: "Break Glass Admin",
      isLocalAuth: true,
      createdAt: new Date("2026-02-28T00:00:00Z"),
    };

    const mockWorkspace = {
      id: "workspace-uuid-123",
    };

    beforeEach(() => {
      process.env.BREAKGLASS_SETUP_TOKEN = "valid-token-123";
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      mockTxUser.create.mockResolvedValue(mockCreatedUser);
      mockTxWorkspace.findFirst.mockResolvedValue(mockWorkspace);
      mockTxWorkspaceMember.create.mockResolvedValue({});
      mockTxSession.create.mockResolvedValue({});
    });

    it("should create a local auth user with hashed password", async () => {
      const result = await service.setup(
        validSetupArgs.email,
        validSetupArgs.name,
        validSetupArgs.password,
        validSetupArgs.setupToken
      );

      expect(result.user).toEqual(mockCreatedUser);
      expect(result.session.token).toBeDefined();
      expect(result.session.token.length).toBeGreaterThan(0);
      expect(result.session.expiresAt).toBeInstanceOf(Date);
      expect(result.session.expiresAt.getTime()).toBeGreaterThan(Date.now());

      expect(mockTxUser.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          email: "admin@example.com",
          name: "Break Glass Admin",
          isLocalAuth: true,
          emailVerified: true,
          passwordHash: expect.any(String) as string,
        }),
        select: {
          id: true,
          email: true,
          name: true,
          isLocalAuth: true,
          createdAt: true,
        },
      });
    });

    it("should assign OWNER role on default workspace", async () => {
      await service.setup(
        validSetupArgs.email,
        validSetupArgs.name,
        validSetupArgs.password,
        validSetupArgs.setupToken
      );

      expect(mockTxWorkspaceMember.create).toHaveBeenCalledWith({
        data: {
          workspaceId: "workspace-uuid-123",
          userId: "user-uuid-123",
          role: "OWNER",
        },
      });
    });

    it("should create a new workspace if none exists", async () => {
      mockTxWorkspace.findFirst.mockResolvedValue(null);
      mockTxWorkspace.create.mockResolvedValue({ id: "new-workspace-uuid" });

      await service.setup(
        validSetupArgs.email,
        validSetupArgs.name,
        validSetupArgs.password,
        validSetupArgs.setupToken
      );

      expect(mockTxWorkspace.create).toHaveBeenCalledWith({
        data: {
          name: "Default Workspace",
          ownerId: "user-uuid-123",
          settings: {},
        },
        select: { id: true },
      });
      expect(mockTxWorkspaceMember.create).toHaveBeenCalledWith({
        data: {
          workspaceId: "new-workspace-uuid",
          userId: "user-uuid-123",
          role: "OWNER",
        },
      });
    });

    it("should create a BetterAuth-compatible session", async () => {
      await service.setup(
        validSetupArgs.email,
        validSetupArgs.name,
        validSetupArgs.password,
        validSetupArgs.setupToken,
        "192.168.1.1",
        "TestAgent/1.0"
      );

      expect(mockTxSession.create).toHaveBeenCalledWith({
        data: {
          userId: "user-uuid-123",
          token: expect.any(String) as string,
          expiresAt: expect.any(Date) as Date,
          ipAddress: "192.168.1.1",
          userAgent: "TestAgent/1.0",
        },
      });
    });

    it("should reject when BREAKGLASS_SETUP_TOKEN is not set", async () => {
      delete process.env.BREAKGLASS_SETUP_TOKEN;

      await expect(
        service.setup(
          validSetupArgs.email,
          validSetupArgs.name,
          validSetupArgs.password,
          validSetupArgs.setupToken
        )
      ).rejects.toThrow(ForbiddenException);
    });

    it("should reject when BREAKGLASS_SETUP_TOKEN is empty", async () => {
      process.env.BREAKGLASS_SETUP_TOKEN = "";

      await expect(
        service.setup(
          validSetupArgs.email,
          validSetupArgs.name,
          validSetupArgs.password,
          validSetupArgs.setupToken
        )
      ).rejects.toThrow(ForbiddenException);
    });

    it("should reject when setup token does not match", async () => {
      await expect(
        service.setup(
          validSetupArgs.email,
          validSetupArgs.name,
          validSetupArgs.password,
          "wrong-token"
        )
      ).rejects.toThrow(ForbiddenException);
    });

    it("should reject when email already exists", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue({
        id: "existing-user",
        email: "admin@example.com",
      });

      await expect(
        service.setup(
          validSetupArgs.email,
          validSetupArgs.name,
          validSetupArgs.password,
          validSetupArgs.setupToken
        )
      ).rejects.toThrow(ConflictException);
    });

    it("should return session token and expiry", async () => {
      const result = await service.setup(
        validSetupArgs.email,
        validSetupArgs.name,
        validSetupArgs.password,
        validSetupArgs.setupToken
      );

      expect(typeof result.session.token).toBe("string");
      expect(result.session.token.length).toBe(64); // 32 bytes hex
      expect(result.session.expiresAt).toBeInstanceOf(Date);
    });
  });

  describe("login", () => {
    const validPasswordHash = "$2a$12$LJ3m4ys3Lz/YgP7xYz5k5uU6b5F6X1234567890abcdefghijkl";

    beforeEach(async () => {
      // Create a real bcrypt hash for testing
      const realHash = await hash("securePassword123!", 4); // Low rounds for test speed
      mockPrismaService.user.findUnique.mockResolvedValue({
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "Break Glass Admin",
        isLocalAuth: true,
        passwordHash: realHash,
        deactivatedAt: null,
      });
      mockPrismaService.session.create.mockResolvedValue({});
    });

    it("should authenticate a valid local auth user", async () => {
      const result = await service.login("admin@example.com", "securePassword123!");

      expect(result.user).toEqual({
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "Break Glass Admin",
      });
      expect(result.session.token).toBeDefined();
      expect(result.session.expiresAt).toBeInstanceOf(Date);
    });

    it("should create a session with ip and user agent", async () => {
      await service.login("admin@example.com", "securePassword123!", "10.0.0.1", "Mozilla/5.0");

      expect(mockPrismaService.session.create).toHaveBeenCalledWith({
        data: {
          userId: "user-uuid-123",
          token: expect.any(String) as string,
          expiresAt: expect.any(Date) as Date,
          ipAddress: "10.0.0.1",
          userAgent: "Mozilla/5.0",
        },
      });
    });

    it("should reject when user does not exist", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);

      await expect(service.login("nonexistent@example.com", "password123456")).rejects.toThrow(
        UnauthorizedException
      );
    });

    it("should reject when user is not a local auth user", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue({
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "OIDC User",
        isLocalAuth: false,
        passwordHash: null,
        deactivatedAt: null,
      });

      await expect(service.login("admin@example.com", "password123456")).rejects.toThrow(
        UnauthorizedException
      );
    });

    it("should reject when user is deactivated", async () => {
      const realHash = await hash("securePassword123!", 4);
      mockPrismaService.user.findUnique.mockResolvedValue({
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "Deactivated User",
        isLocalAuth: true,
        passwordHash: realHash,
        deactivatedAt: new Date("2026-01-01"),
      });

      await expect(service.login("admin@example.com", "securePassword123!")).rejects.toThrow(
        new UnauthorizedException("Account has been deactivated")
      );
    });

    it("should reject when password is incorrect", async () => {
      await expect(service.login("admin@example.com", "wrongPassword123!")).rejects.toThrow(
        UnauthorizedException
      );
    });

    it("should throw InternalServerError when local auth user has no password hash", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue({
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "Broken User",
        isLocalAuth: true,
        passwordHash: null,
        deactivatedAt: null,
      });

      await expect(service.login("admin@example.com", "securePassword123!")).rejects.toThrow(
        InternalServerErrorException
      );
    });

    it("should not reveal whether email exists in error messages", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);

      try {
        await service.login("nonexistent@example.com", "password123456");
      } catch (error) {
        expect(error).toBeInstanceOf(UnauthorizedException);
        expect((error as UnauthorizedException).message).toBe("Invalid email or password");
      }
    });
  });
});
