import 'reflect-metadata';

import { ValidationPipe } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { requestContextMiddleware } from '@adapters/http/RequestContext';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import { IDENTITY_SESSION_PORT_TOKEN } from '../auth/AuthProviders';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';

describe('League schedule admin CRUD (HTTP, season-scoped)', () => {
  const originalEnv = { ...process.env };

  let app: import("@nestjs/common").INestApplication;

  beforeAll(async () => {
    vi.resetModules();

    process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
    process.env.GRIDPILOT_API_BOOTSTRAP = 'true';
    delete process.env.DATABASE_URL;

    const { AppModule } = await import('../../app.module');

    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = module.createNestApplication();

    // Ensure AsyncLocalStorage request context is present for getActorFromRequestContext()
    app.use(requestContextMiddleware);

    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );

    const reflector = new Reflector();
    const sessionPort = module.get(IDENTITY_SESSION_PORT_TOKEN);

    const authorizationService = {
      getRolesForUser: () => [],
    };

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

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

    await app.init();
  }, 20_000);

  afterAll(async () => {
    await app?.close();

    process.env = originalEnv;
    vi.restoreAllMocks();
  });

  it('rejects unauthenticated actor (401)', async () => {
    await request(app.getHttpServer())
      .post('/leagues/league-5/seasons/league-5-season-1/schedule/races')
      .send({
        track: 'Test Track',
        car: 'Test Car',
        scheduledAtIso: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
      })
      .expect(401);
  });

  it('rejects authenticated non-admin actor (403)', async () => {
    const agent = request.agent(app.getHttpServer());

    await agent
      .post('/auth/signup')
      .send({ email: 'user1@gridpilot.local', password: 'Password123!', displayName: 'John Smith' })
      .expect(201);

    await agent
      .post('/leagues/league-5/seasons/league-5-season-1/schedule/races')
      .send({
        track: 'Test Track',
        car: 'Test Car',
        scheduledAtIso: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
      })
      .expect(403);
  });

  it('rejects payload identity spoofing (400)', async () => {
    const agent = request.agent(app.getHttpServer());

    await agent
      .post('/auth/login')
      .send({ email: 'admin@gridpilot.local', password: 'admin123' })
      .expect(201);

    await agent
      .post('/leagues/league-5/seasons/season-1/schedule/races')
      .send({
        track: 'Test Track',
        car: 'Test Car',
        scheduledAtIso: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
        performerDriverId: 'driver-1',
      })
      .expect(400);
  });

  it('create/update/delete changes schedule read for the same season (happy path)', async () => {
    const agent = request.agent(app.getHttpServer());

    await agent
      .post('/auth/login')
      .send({ email: 'admin@gridpilot.local', password: 'admin123' })
      .expect(201);

    const initialScheduleRes = await agent.get('/leagues/league-5/schedule?seasonId=league-5-season-1').expect(200);
    expect(initialScheduleRes.body).toMatchObject({ seasonId: 'league-5-season-1', races: expect.any(Array) });

    // Try a date further in the future (100 days)
    const scheduledAtIso = new Date(Date.now() + 100 * 24 * 60 * 60 * 1000).toISOString();

    const createRes = await agent
      .post('/leagues/league-5/seasons/league-5-season-1/schedule/races')
      .send({
        track: 'Test Track',
        car: 'Test Car',
        scheduledAtIso,
      })
      .expect(201);

    expect(createRes.body).toMatchObject({ raceId: expect.any(String) });
    const raceId: string = createRes.body.raceId;

    const afterCreateRes = await agent.get('/leagues/league-5/schedule?seasonId=league-5-season-1').expect(200);
    const createdRace = (afterCreateRes.body.races as { id: string }[]).find((r) => r.id === raceId);
    expect(createdRace).toMatchObject({
      id: raceId,
      name: 'Test Track - Test Car',
      date: scheduledAtIso,
    });

    const updatedAtIso = new Date(Date.now() + 105 * 24 * 60 * 60 * 1000).toISOString();

    await agent
      .patch(`/leagues/league-5/seasons/league-5-season-1/schedule/races/${raceId}`)
      .send({
        track: 'Updated Track',
        car: 'Updated Car',
        scheduledAtIso: updatedAtIso,
      })
      .expect(200)
      .expect((res) => {
        expect(res.body).toEqual({ success: true });
      });

    const afterUpdateRes = await agent.get('/leagues/league-5/schedule?seasonId=league-5-season-1').expect(200);
    const updatedRace = (afterUpdateRes.body.races as { id: string }[]).find((r) => r.id === raceId);
    expect(updatedRace).toMatchObject({
      id: raceId,
      name: 'Updated Track - Updated Car',
      date: updatedAtIso,
    });

    await agent.delete(`/leagues/league-5/seasons/league-5-season-1/schedule/races/${raceId}`).expect(200).expect({
      success: true,
    });

    const afterDeleteRes = await agent.get('/leagues/league-5/schedule?seasonId=league-5-season-1').expect(200);
    const deletedRace = (afterDeleteRes.body.races as { id: string }[]).find((r) => r.id === raceId);
    expect(deletedRace).toBeUndefined();
  });
});