import 'reflect-metadata';

import { Controller, Get } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import { PolicyService, type PolicySnapshot } from '../policy/PolicyService';
import { RequireCapability } from '../policy/RequireCapability';
import { AuthenticationGuard } from './AuthenticationGuard';
import { AuthorizationGuard } from './AuthorizationGuard';
import { AuthorizationService } from './AuthorizationService';
import { Public } from './Public';
import { RequireRoles } from './RequireRoles';

@Controller('authz-test')
class AuthzTestController {
  @Public()
  @Get('public')
  publicRoute() {
    return { ok: true };
  }

  @Get('protected')
  protectedRoute() {
    return { ok: true };
  }

  @RequireRoles('admin')
  @Get('admin')
  adminOnlyRoute() {
    return { ok: true };
  }

  @RequireCapability('demo.feature', 'view')
  @Get('feature')
  featureGatedRoute() {
    return { ok: true };
  }
}

type SessionPort = {
  getCurrentSession: () => Promise<null | { token: string; user: { id: string } }>;
};

describe('Auth guards (HTTP)', () => {
  let app: import("@nestjs/common").INestApplication;

  const sessionPort: SessionPort = {
    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: { 'demo.feature': 'enabled' },
      loadedFrom: 'defaults',
      loadedAtIso: new Date(0).toISOString(),
    })),
  };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [AuthzTestController],
    }).compile();

    app = module.createNestApplication();

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

    await app.init();
  });

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

  it('allows `@Public()` routes without a session', async () => {
    await request(app.getHttpServer()).get('/authz-test/public').expect(200).expect({ ok: true });
    expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
  });

  it('denies non-public routes by default when not authenticated (401)', async () => {
    await request(app.getHttpServer()).get('/authz-test/protected').expect(401);
  });

  it('allows non-public routes when authenticated via session port', async () => {
    vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
      token: 't',
      user: { id: 'user-1' },
    });

    await request(app.getHttpServer()).get('/authz-test/protected').expect(200).expect({ ok: true });
  });

  it('returns 403 when route requires a role and the user does not have it', async () => {
    vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
      token: 't',
      user: { id: 'user-1' },
    });
    vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['user']);

    await request(app.getHttpServer()).get('/authz-test/admin').expect(403);
  });

  it('allows access when route requires a role and the user has it', async () => {
    vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
      token: 't',
      user: { id: 'user-1' },
    });
    vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']);

    await request(app.getHttpServer()).get('/authz-test/admin').expect(200).expect({ ok: true });
  });

  it('returns 404 when a `@RequireCapability()` feature is disabled', async () => {
    vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
      token: 't',
      user: { id: 'user-1' },
    });

    vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
      policyVersion: 1,
      operationalMode: 'normal',
      maintenanceAllowlist: { view: [], mutate: [] },
      capabilities: { 'demo.feature': 'disabled' },
      loadedFrom: 'env',
      loadedAtIso: new Date(0).toISOString(),
    });

    await request(app.getHttpServer()).get('/authz-test/feature').expect(404);
  });

  it('returns 503 during maintenance when capability is not allowlisted', async () => {
    vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
      token: 't',
      user: { id: 'user-1' },
    });

    vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
      policyVersion: 1,
      operationalMode: 'maintenance',
      maintenanceAllowlist: { view: [], mutate: [] },
      capabilities: { 'demo.feature': 'enabled' },
      loadedFrom: 'env',
      loadedAtIso: new Date(0).toISOString(),
    });

    await request(app.getHttpServer()).get('/authz-test/feature').expect(503);
  });
});