import 'reflect-metadata';

import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { Mock, vi } from 'vitest';
import { AuthController } from './AuthController';
import { AuthService } from './AuthService';
import { AuthSessionDTO, LoginParamsDTO, SignupParamsDTO } from './dtos/AuthDto';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
import { AuthenticationGuard } from './AuthenticationGuard';
import { AuthorizationGuard } from './AuthorizationGuard';
import type { AuthorizationService } from './AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';

describe('AuthController', () => {
  let controller: AuthController;
  let service: AuthService;

  beforeEach(() => {
    service = {
      signupWithEmail: vi.fn(),
      loginWithEmail: vi.fn(),
      getCurrentSession: vi.fn(),
      logout: vi.fn(),
    } as unknown as AuthService;

    controller = new AuthController(service);
  });

  describe('signup', () => {
    it('should call service.signupWithEmail and return session DTO', async () => {
      const params: SignupParamsDTO = {
        email: 'test@example.com',
        password: 'password123',
        displayName: 'Test User',
        iracingCustomerId: '12345',
        primaryDriverId: 'driver1',
        avatarUrl: 'http://example.com/avatar.jpg',
      };
      const session: AuthSessionDTO = {
        token: 'token123',
        user: {
          userId: 'user1',
          email: 'test@example.com',
          displayName: 'Test User',
        },
      };
      (service.signupWithEmail as Mock).mockResolvedValue(session);

      const result = await controller.signup(params);

      expect(service.signupWithEmail).toHaveBeenCalledWith(params);
      expect(result).toEqual(session);
    });
  });

  describe('login', () => {
    it('should call service.loginWithEmail and return session DTO', async () => {
      const params: LoginParamsDTO = {
        email: 'test@example.com',
        password: 'password123',
      };
      const session: AuthSessionDTO = {
        token: 'token123',
        user: {
          userId: 'user1',
          email: 'test@example.com',
          displayName: 'Test User',
        },
      };
      (service.loginWithEmail as Mock).mockResolvedValue(session);

      const result = await controller.login(params);

      expect(service.loginWithEmail).toHaveBeenCalledWith(params);
      expect(result).toEqual(session);
    });
  });

  describe('getSession', () => {
    it('should call service.getCurrentSession and write JSON response', async () => {
      const session: AuthSessionDTO = {
        token: 'token123',
        user: {
          userId: 'user1',
          email: 'test@example.com',
          displayName: 'Test User',
        },
      };
      (service.getCurrentSession as Mock).mockResolvedValue(session);

      const res = { json: vi.fn() } as any;

      await controller.getSession(res);

      expect(service.getCurrentSession).toHaveBeenCalled();
      expect(res.json).toHaveBeenCalledWith(session);
    });

    it('should write JSON null when no session', async () => {
      (service.getCurrentSession as Mock).mockResolvedValue(null);

      const res = { json: vi.fn() } as any;

      await controller.getSession(res);

      expect(service.getCurrentSession).toHaveBeenCalled();
      expect(res.json).toHaveBeenCalledWith(null);
    });
  });

  describe('logout', () => {
    it('should call service.logout and return DTO', async () => {
      const dto: CommandResultDTO = { success: true };
      (service.logout as Mock).mockResolvedValue(dto);

      const result = await controller.logout();

      expect(service.logout).toHaveBeenCalled();
      expect(result).toEqual(dto);
    });
  });

  describe('auth guards (HTTP)', () => {
    let app: any;

    const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
      getCurrentSession: vi.fn(async () => null),
    };

    const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
      getRolesForUser: vi.fn(() => []),
    };

    const policyService: Pick<PolicyService, 'getSnapshot'> = {
      getSnapshot: vi.fn(async (): Promise<PolicySnapshot> => ({
        policyVersion: 1,
        operationalMode: 'normal',
        maintenanceAllowlist: { view: [], mutate: [] },
        capabilities: {},
        loadedFrom: 'defaults',
        loadedAtIso: new Date(0).toISOString(),
      })),
    };

    beforeEach(async () => {
      const module = await Test.createTestingModule({
        controllers: [AuthController],
        providers: [
          {
            provide: AuthService,
            useValue: {
              getCurrentSession: vi.fn(async () => null),
              loginWithEmail: vi.fn(),
              signupWithEmail: vi.fn(),
              logout: vi.fn(),
              startIracingAuth: vi.fn(),
              iracingCallback: vi.fn(),
            },
          },
        ],
      })
        .overrideProvider(AuthController)
        .useFactory({
          factory: (authService: AuthService) => new AuthController(authService),
          inject: [AuthService],
        })
        .compile();

      app = module.createNestApplication();

      const reflector = new Reflector();
      app.useGlobalGuards(
        new AuthenticationGuard(sessionPort as any),
        new AuthorizationGuard(reflector, authorizationService as any),
        new FeatureAvailabilityGuard(reflector, policyService as any),
      );

      await app.init();
    });

    afterEach(async () => {
      await app?.close();
      vi.clearAllMocks();
    });

    it('allows @Public() endpoint without a session', async () => {
      await request(app.getHttpServer()).get('/auth/session').expect(200);
    });
  });
});