import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FeatureAvailabilityGuard, inferActionTypeFromHttpMethod } from './FeatureAvailabilityGuard';
import { PolicyService } from './PolicyService';
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';

class MockReflector {
  getAllAndOverride = vi.fn();
}

class MockPolicyService {
  getSnapshot = vi.fn();
}

describe('FeatureAvailabilityGuard', () => {
  let guard: FeatureAvailabilityGuard;
  let reflector: MockReflector;
  let policyService: MockPolicyService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        FeatureAvailabilityGuard,
        {
          provide: Reflector,
          useClass: MockReflector,
        },
        {
          provide: PolicyService,
          useClass: MockPolicyService,
        },
      ],
    }).compile();

    guard = module.get<FeatureAvailabilityGuard>(FeatureAvailabilityGuard);
    reflector = module.get<Reflector>(Reflector) as unknown as MockReflector;
    policyService = module.get<PolicyService>(PolicyService) as unknown as MockPolicyService;
  });

  describe('canActivate', () => {
    it('should return true when no metadata is found', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      reflector.getAllAndOverride.mockReturnValue(undefined);

      const result = await guard.canActivate(mockContext);

      expect(result).toBe(true);
      expect(reflector.getAllAndOverride).toHaveBeenCalledWith(
        FEATURE_AVAILABILITY_METADATA_KEY,
        [mockContext.getHandler(), mockContext.getClass()]
      );
    });

    it('should return true when feature is enabled', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'view',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'normal',
        capabilities: { 'test-feature': 'enabled' },
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      const result = await guard.canActivate(mockContext);

      expect(result).toBe(true);
    });

    it('should throw ServiceUnavailableException when in maintenance mode and not in allowlist', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'mutate',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'maintenance',
        capabilities: { 'test-feature': 'enabled' },
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      await expect(guard.canActivate(mockContext)).rejects.toThrow(ServiceUnavailableException);
      await expect(guard.canActivate(mockContext)).rejects.toThrow('Service temporarily unavailable');
    });

    it('should return true when in maintenance mode but in allowlist', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'mutate',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'maintenance',
        capabilities: { 'test-feature': 'enabled' },
        maintenanceAllowlist: { mutate: ['test-feature'], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      const result = await guard.canActivate(mockContext);

      expect(result).toBe(true);
    });

    it('should throw NotFoundException when feature is disabled', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'view',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'normal',
        capabilities: { 'test-feature': 'disabled' },
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
      await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
    });

    it('should throw NotFoundException when feature is hidden', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'view',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'normal',
        capabilities: { 'test-feature': 'hidden' },
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
      await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
    });

    it('should throw NotFoundException when feature is coming_soon', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'view',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'normal',
        capabilities: { 'test-feature': 'coming_soon' },
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
      await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
    });

    it('should throw NotFoundException when feature is not configured', async () => {
      const mockContext = {
        getHandler: () => () => {},
        getClass: () => class {},
      } as unknown as ExecutionContext;

      const metadata: FeatureAvailabilityMetadata = {
        capabilityKey: 'test-feature',
        actionType: 'view',
      };

      reflector.getAllAndOverride.mockReturnValue(metadata);
      policyService.getSnapshot.mockResolvedValue({
        policyVersion: 1,
        operationalMode: 'normal',
        capabilities: {},
        maintenanceAllowlist: { mutate: [], view: [] },
        loadedFrom: 'defaults',
        loadedAtIso: new Date().toISOString(),
      });

      await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
      await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
    });
  });

  describe('inferActionTypeFromHttpMethod', () => {
    it('should return "view" for GET requests', () => {
      expect(inferActionTypeFromHttpMethod('GET')).toBe('view');
    });

    it('should return "view" for HEAD requests', () => {
      expect(inferActionTypeFromHttpMethod('HEAD')).toBe('view');
    });

    it('should return "view" for OPTIONS requests', () => {
      expect(inferActionTypeFromHttpMethod('OPTIONS')).toBe('view');
    });

    it('should return "mutate" for POST requests', () => {
      expect(inferActionTypeFromHttpMethod('POST')).toBe('mutate');
    });

    it('should return "mutate" for PUT requests', () => {
      expect(inferActionTypeFromHttpMethod('PUT')).toBe('mutate');
    });

    it('should return "mutate" for PATCH requests', () => {
      expect(inferActionTypeFromHttpMethod('PATCH')).toBe('mutate');
    });

    it('should return "mutate" for DELETE requests', () => {
      expect(inferActionTypeFromHttpMethod('DELETE')).toBe('mutate');
    });

    it('should handle lowercase HTTP methods', () => {
      expect(inferActionTypeFromHttpMethod('get')).toBe('view');
      expect(inferActionTypeFromHttpMethod('post')).toBe('mutate');
    });
  });
});