import { config } from 'dotenv';
import { resolve } from 'path';
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, Module } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
import { CharactersModule } from './../src/characters/characters.module';
import { DatabaseModule } from './../src/database/database.module';
import { configureApplication } from './../src/app.config';
import { TravelModule } from './../src/travel/travel.module';
import { WorldModule } from './../src/world/world.module';
import { DEMO_CHARACTER_ID } from './../src/demo/demo-character.constants';
import { BURNED_ROAD_ID } from './../src/database/seeds/vertical-slice.constants';

// `npm run test:e2e` runs from the `apps/api` workspace, so process.cwd()
// is `apps/api`, not the repo root where the documented `.env` lives.
// Load it the same way `main.ts`/`data-source.ts` do, without overriding a
// `DATABASE_URL` a developer or CI already exported.
config({ path: resolve(__dirname, '../../../.env') });

@Module({})
class TestDatabaseModule {}

@Module({})
class TestCharactersModule {}

@Module({})
class TestTravelModule {}

@Module({})
class TestWorldModule {}

describe('Visible vertical slice smoke (e2e)', () => {
  describe('without a developer database', () => {
    let app: INestApplication<App>;

    beforeEach(async () => {
      const moduleFixture: TestingModule = await Test.createTestingModule({
        imports: [AppModule],
      })
        .overrideModule(DatabaseModule)
        .useModule(TestDatabaseModule)
        .overrideModule(CharactersModule)
        .useModule(TestCharactersModule)
        .overrideModule(TravelModule)
        .useModule(TestTravelModule)
        .overrideModule(WorldModule)
        .useModule(TestWorldModule)
        .compile();

      app = moduleFixture.createNestApplication();
      configureApplication(app);
      await app.init();
    });

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

    it('GET /api/health returns ok', () => {
      return request(app.getHttpServer())
        .get('/api/health')
        .expect(200)
        .expect({ status: 'ok' });
    });
  });

  // These assertions exercise the real DatabaseModule, TypeORM entities and
  // the seeded demo character/world data, so they only run when a reachable
  // PostgreSQL database is configured (see README.md for local setup).
  const describeWithDatabase = process.env.DATABASE_URL
    ? describe
    : describe.skip;

  describeWithDatabase('against a migrated and seeded database', () => {
    let app: INestApplication<App>;

    beforeAll(async () => {
      const moduleFixture: TestingModule = await Test.createTestingModule({
        imports: [AppModule],
      }).compile();

      app = moduleFixture.createNestApplication();
      configureApplication(app);
      await app.init();
    });

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

    it('GET /api/health returns ok', () => {
      return request(app.getHttpServer())
        .get('/api/health')
        .expect(200)
        .expect({ status: 'ok' });
    });

    it('GET /api/characters/me returns the seeded demo character', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/characters/me')
        .expect(200);
      const body = response.body as {
        id: string;
        name: string;
        currentLocation: { id: string; key: string; name: string };
      };

      expect(body).toMatchObject({
        id: DEMO_CHARACTER_ID,
        name: 'Aric Duskwalker',
      });
      expect(typeof body.currentLocation.id).toBe('string');
      expect(typeof body.currentLocation.key).toBe('string');
      expect(typeof body.currentLocation.name).toBe('string');
    });

    it('GET /api/world/current-location returns the character location with its connections', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/world/current-location')
        .expect(200);
      const body = response.body as {
        id: string;
        key: string;
        name: string;
        connections: unknown[];
      };

      expect(typeof body.id).toBe('string');
      expect(typeof body.key).toBe('string');
      expect(typeof body.name).toBe('string');
      expect(Array.isArray(body.connections)).toBe(true);
      expect(body.connections.length).toBeGreaterThan(0);
    });

    it('GET /api/travel/current returns a known travel status', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/travel/current')
        .expect(200);
      const body = response.body as { status: string };

      expect(['IDLE', 'TRAVELLING', 'COMPLETED']).toContain(body.status);
    });

    it('POST /api/travel rejects a body carrying a non-whitelisted arrivesAt field', () => {
      return request(app.getHttpServer())
        .post('/api/travel')
        .send({
          targetLocationId: BURNED_ROAD_ID,
          arrivesAt: new Date().toISOString(),
        })
        .expect(400);
    });
  });
});
