import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import {
  NotFoundException,
  ForbiddenException,
  UnauthorizedException,
  ConflictException,
} from "@nestjs/common";
import { LocalAuthController } from "./local-auth.controller";
import { LocalAuthService } from "./local-auth.service";
import { LocalAuthEnabledGuard } from "./local-auth.guard";

describe("LocalAuthController", () => {
  let controller: LocalAuthController;
  let localAuthService: LocalAuthService;

  const mockLocalAuthService = {
    setup: vi.fn(),
    login: vi.fn(),
  };

  const mockRequest = {
    headers: { "user-agent": "TestAgent/1.0" },
    ip: "127.0.0.1",
    socket: { remoteAddress: "127.0.0.1" },
  };

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

  beforeEach(async () => {
    process.env.ENABLE_LOCAL_AUTH = "true";

    const module: TestingModule = await Test.createTestingModule({
      controllers: [LocalAuthController],
      providers: [
        {
          provide: LocalAuthService,
          useValue: mockLocalAuthService,
        },
      ],
    })
      .overrideGuard(LocalAuthEnabledGuard)
      .useValue({ canActivate: () => true })
      .compile();

    controller = module.get<LocalAuthController>(LocalAuthController);
    localAuthService = module.get<LocalAuthService>(LocalAuthService);

    vi.clearAllMocks();
  });

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

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

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

    it("should create a break-glass user and return user data with session", async () => {
      mockLocalAuthService.setup.mockResolvedValue(mockSetupResult);

      const result = await controller.setup(setupDto, mockRequest as never);

      expect(result).toEqual({
        user: mockSetupResult.user,
        session: mockSetupResult.session,
      });
      expect(mockLocalAuthService.setup).toHaveBeenCalledWith(
        "admin@example.com",
        "Break Glass Admin",
        "securePassword123!",
        "valid-token-123",
        "127.0.0.1",
        "TestAgent/1.0"
      );
    });

    it("should extract client IP from x-forwarded-for header", async () => {
      mockLocalAuthService.setup.mockResolvedValue(mockSetupResult);
      const reqWithProxy = {
        ...mockRequest,
        headers: {
          ...mockRequest.headers,
          "x-forwarded-for": "203.0.113.50, 70.41.3.18",
        },
      };

      await controller.setup(setupDto, reqWithProxy as never);

      expect(mockLocalAuthService.setup).toHaveBeenCalledWith(
        expect.any(String) as string,
        expect.any(String) as string,
        expect.any(String) as string,
        expect.any(String) as string,
        "203.0.113.50",
        "TestAgent/1.0"
      );
    });

    it("should propagate ForbiddenException from service", async () => {
      mockLocalAuthService.setup.mockRejectedValue(new ForbiddenException("Invalid setup token"));

      await expect(controller.setup(setupDto, mockRequest as never)).rejects.toThrow(
        ForbiddenException
      );
    });

    it("should propagate ConflictException from service", async () => {
      mockLocalAuthService.setup.mockRejectedValue(
        new ConflictException("A user with this email already exists")
      );

      await expect(controller.setup(setupDto, mockRequest as never)).rejects.toThrow(
        ConflictException
      );
    });
  });

  describe("login", () => {
    const loginDto = {
      email: "admin@example.com",
      password: "securePassword123!",
    };

    const mockLoginResult = {
      user: {
        id: "user-uuid-123",
        email: "admin@example.com",
        name: "Break Glass Admin",
      },
      session: {
        token: "session-token-abc",
        expiresAt: new Date("2026-03-07T00:00:00Z"),
      },
    };

    it("should authenticate and return user data with session", async () => {
      mockLocalAuthService.login.mockResolvedValue(mockLoginResult);

      const result = await controller.login(loginDto, mockRequest as never);

      expect(result).toEqual({
        user: mockLoginResult.user,
        session: mockLoginResult.session,
      });
      expect(mockLocalAuthService.login).toHaveBeenCalledWith(
        "admin@example.com",
        "securePassword123!",
        "127.0.0.1",
        "TestAgent/1.0"
      );
    });

    it("should propagate UnauthorizedException from service", async () => {
      mockLocalAuthService.login.mockRejectedValue(
        new UnauthorizedException("Invalid email or password")
      );

      await expect(controller.login(loginDto, mockRequest as never)).rejects.toThrow(
        UnauthorizedException
      );
    });
  });
});

describe("LocalAuthEnabledGuard", () => {
  let guard: LocalAuthEnabledGuard;

  const originalEnv = process.env.ENABLE_LOCAL_AUTH;

  beforeEach(() => {
    guard = new LocalAuthEnabledGuard();
  });

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

  it("should allow access when ENABLE_LOCAL_AUTH is true", () => {
    process.env.ENABLE_LOCAL_AUTH = "true";

    expect(guard.canActivate()).toBe(true);
  });

  it("should throw NotFoundException when ENABLE_LOCAL_AUTH is not set", () => {
    delete process.env.ENABLE_LOCAL_AUTH;

    expect(() => guard.canActivate()).toThrow(NotFoundException);
  });

  it("should throw NotFoundException when ENABLE_LOCAL_AUTH is false", () => {
    process.env.ENABLE_LOCAL_AUTH = "false";

    expect(() => guard.canActivate()).toThrow(NotFoundException);
  });

  it("should throw NotFoundException when ENABLE_LOCAL_AUTH is empty", () => {
    process.env.ENABLE_LOCAL_AUTH = "";

    expect(() => guard.canActivate()).toThrow(NotFoundException);
  });
});
