import 'reflect-metadata';
import {
  type CanActivate,
  type ExecutionContext,
  type INestApplication,
  ValidationPipe,
} from '@nestjs/common';
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { AuthGuard } from '../auth/auth.guard.js';
import { HarnessRegistry } from './harness.registry.js';
import { HARNESS_REGISTRY } from './harness.tokens.js';
import { HarnessSelectionRepository } from './harness-selection.repository.js';
import { FakeHarnessAdapter } from './testing/fake-harness.adapter.js';
// Import the REAL module (not a hand-listed controllers+mocks list) so an
// unresolved provider fails at app.init() — the #1145-class DI-boot guard.
import { HarnessModule } from './harness.module.js';

// A known-available tuple from the fake adapter's default catalog.
const VALID = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-mini' };
// A tuple whose provider/model are not in any catalog.
const UNKNOWN = { harnessId: 'fake', providerId: 'ghost-provider', modelId: 'ghost-model' };
// A tuple that is known in the catalog but flagged unavailable.
const UNAVAILABLE = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-legacy' };

const authGuard: CanActivate = {
  canActivate(context: ExecutionContext): boolean {
    const requestContext = context.switchToHttp().getRequest<{ user?: { id: string } }>();
    requestContext.user = { id: 'user-1' };
    return true;
  },
};

function registryWithFake(): HarnessRegistry {
  const registry = new HarnessRegistry();
  registry.register(new FakeHarnessAdapter({ id: 'fake' }));
  return registry;
}

describe('Harness selection HTTP surface', () => {
  let app: INestApplication;
  let repository: HarnessSelectionRepository;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [HarnessModule],
    })
      .overrideGuard(AuthGuard)
      .useValue(authGuard)
      .overrideProvider(HARNESS_REGISTRY)
      .useValue(registryWithFake())
      .compile();

    // Real in-memory repository from the module graph — proves the module wired it.
    repository = moduleRef.get(HarnessSelectionRepository);

    app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
    app.useGlobalPipes(
      new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
    );
    await app.init();
    await app.getHttpAdapter().getInstance().ready();
  });

  beforeEach(() => {
    // Reset owner-scoped state between tests via the public API surface.
    repository.set({ userId: 'user-1', tenantId: 'user-1' }, VALID);
  });

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

  it('GET selection is server-scoped and ignores caller-supplied scope in the query', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/chat/preferences/selection')
      .query({ userId: 'attacker', tenantId: 'attacker-tenant', seatId: 'attacker-seat' });

    expect(response.status).toBe(200);
    // The returned selection is user-1's (guard-derived scope), not the query's.
    expect(response.body.selection).toEqual(VALID);
  });

  it('PUT with a valid structured tuple persists and round-trips via GET', async () => {
    const next = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-pro' };

    const put = await request(app.getHttpServer())
      .put('/api/chat/preferences/selection')
      .send(next)
      .set('Content-Type', 'application/json');
    expect(put.status).toBe(200);
    expect(put.body.selection).toEqual(next);

    const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection');
    expect(get.status).toBe(200);
    expect(get.body.selection).toEqual(next);
  });

  it('PUT with FREE TEXT is rejected 400 and does not mutate the stored selection', async () => {
    const response = await request(app.getHttpServer())
      .put('/api/chat/preferences/selection')
      .send({ selection: 'gpt-4o' })
      .set('Content-Type', 'application/json');

    expect(response.status).toBe(400);

    const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection');
    expect(get.body.selection).toEqual(VALID);
  });

  it.each([
    ['seatId', { ...VALID, seatId: 'attacker-seat' }],
    ['tenantId', { ...VALID, tenantId: 'attacker-tenant' }],
    ['userId', { ...VALID, userId: 'attacker' }],
    ['nativeSessionPath', { ...VALID, nativeSessionPath: '/var/native/x.jsonl' }],
    ['executable', { ...VALID, executable: '/usr/bin/evil' }],
    ['home', { ...VALID, home: '/home/attacker' }],
    ['cwd', { ...VALID, cwd: '/tmp/attacker' }],
  ])(
    'PUT with an extra authority-bearing field (%s) is rejected 400 and does not mutate stored selection',
    async (_name, body) => {
      const response = await request(app.getHttpServer())
        .put('/api/chat/preferences/selection')
        .send(body)
        .set('Content-Type', 'application/json');

      expect(response.status).toBe(400);

      const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection');
      expect(get.body.selection).toEqual(VALID);
    },
  );

  it('PUT with an UNKNOWN tuple returns selection_invalid, unchanged and echoed unchanged (no fallback)', async () => {
    const response = await request(app.getHttpServer())
      .put('/api/chat/preferences/selection')
      .send(UNKNOWN)
      .set('Content-Type', 'application/json');

    expect(response.status).toBe(422);
    expect(response.body.code).toBe('selection_invalid');
    // Echoed back unchanged: no first-row / first-provider substitution.
    expect(response.body.selection).toEqual(UNKNOWN);

    const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection');
    expect(get.body.selection).toEqual(VALID);
  });

  it('PUT with a KNOWN-but-UNAVAILABLE tuple returns model_unavailable, unchanged (distinct from selection_invalid)', async () => {
    const response = await request(app.getHttpServer())
      .put('/api/chat/preferences/selection')
      .send(UNAVAILABLE)
      .set('Content-Type', 'application/json');

    expect(response.status).toBe(422);
    expect(response.body.code).toBe('model_unavailable');
    expect(response.body.selection).toEqual(UNAVAILABLE);

    const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection');
    expect(get.body.selection).toEqual(VALID);
  });
});
