import 'reflect-metadata';

import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { vi } from 'vitest';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
import type { Response } from 'express';
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';

describe('AnalyticsController', () => {
  let controller: AnalyticsController;
  let service: ReturnType<typeof vi.mocked<AnalyticsService>>;

  beforeEach(() => {
    service = {
      recordPageView: vi.fn(),
      recordEngagement: vi.fn(),
      getDashboardData: vi.fn(),
      getAnalyticsMetrics: vi.fn(),
    } as any;
    controller = new AnalyticsController(service);
  });

  describe('recordPageView', () => {
    it('should record a page view and return 201', async () => {
      const input = {
        entityType: EntityType.RACE,
        entityId: 'race-123',
        visitorType: VisitorType.ANONYMOUS,
        sessionId: 'session-456',
        visitorId: 'visitor-789',
        referrer: 'https://example.com',
        userAgent: 'Mozilla/5.0',
        country: 'US',
      };
      const dto: RecordPageViewOutputDTO = { pageViewId: 'pv-123' };
      service.recordPageView.mockResolvedValue(dto);

      const mockRes: ReturnType<typeof vi.mocked<Response>> = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as ReturnType<typeof vi.mocked<Response>>;

      await controller.recordPageView(input, mockRes);

      expect(service.recordPageView).toHaveBeenCalledWith(input);
      expect(mockRes.status).toHaveBeenCalledWith(201);
      expect(mockRes.json).toHaveBeenCalledWith(dto);
    });
  });

  describe('recordEngagement', () => {
    it('should record an engagement and return 201', async () => {
      const input = {
        action: EngagementAction.CLICK_SPONSOR_LOGO,
        entityType: EngagementEntityType.RACE,
        entityId: 'race-123',
        actorType: 'driver' as const,
        sessionId: 'session-456',
        actorId: 'actor-789',
        metadata: { key: 'value' },
      };
      const dto: RecordEngagementOutputDTO = { eventId: 'event-123', engagementWeight: 10 };
      service.recordEngagement.mockResolvedValue(dto);

      const mockRes: ReturnType<typeof vi.mocked<Response>> = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as ReturnType<typeof vi.mocked<Response>>;

      await controller.recordEngagement(input, mockRes);

      expect(service.recordEngagement).toHaveBeenCalledWith(input);
      expect(mockRes.status).toHaveBeenCalledWith(201);
      expect(mockRes.json).toHaveBeenCalledWith(dto);
    });
  });

  describe('getDashboardData', () => {
    it('should return dashboard data', async () => {
      const dto = {
        totalUsers: 100,
        activeUsers: 50,
        totalRaces: 20,
        totalLeagues: 5,
      };
      service.getDashboardData.mockResolvedValue(dto);

      const result = await controller.getDashboardData();

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

  describe('getAnalyticsMetrics', () => {
    it('should return analytics metrics', async () => {
      const dto = {
        pageViews: 1000,
        uniqueVisitors: 500,
        averageSessionDuration: 300,
        bounceRate: 0.4,
      };
      service.getAnalyticsMetrics.mockResolvedValue(dto);

      const result = await controller.getAnalyticsMetrics();

      expect(service.getAnalyticsMetrics).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: [AnalyticsController],
        providers: [
          {
            provide: AnalyticsService,
            useValue: {
              recordPageView: vi.fn(async () => ({})),
              recordEngagement: vi.fn(async () => ({})),
              getDashboardData: vi.fn(async () => ({})),
              getAnalyticsMetrics: vi.fn(async () => ({})),
            },
          },
        ],
      })
        .overrideProvider(AnalyticsController)
        .useFactory({
          factory: (analyticsService: AnalyticsService) => new AnalyticsController(analyticsService),
          inject: [AnalyticsService],
        })
        .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', { retry: 2 }, async () => {
      await request(app.getHttpServer()).post('/analytics/page-view').send({}).expect(201);
    });

    it('denies internal dashboard endpoint by default when not authenticated (401)', async () => {
      await request(app.getHttpServer()).get('/analytics/dashboard').expect(401);
    });

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

      await request(app.getHttpServer()).get('/analytics/dashboard').expect(200);
    });
  });
});