/**
 * Test suite for type generation script
 * Validates that the type generation process works correctly
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execSync } from 'child_process';
import { createHash } from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';

describe('Type Generation Script', () => {
  const apiRoot = path.join(__dirname, '../../apps/api');
  const websiteRoot = path.join(__dirname, '../../apps/website');
  const openapiPath = path.join(apiRoot, 'openapi.json');
  const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated');
  const backupDir = path.join(__dirname, '../../.backup/type-gen-test');

  async function sha256OfFile(filePath: string): Promise<string> {
    const buffer = await fs.readFile(filePath);
    return createHash('sha256').update(buffer).digest('hex');
  }

  beforeAll(async () => {
    // Backup existing generated types
    await fs.mkdir(backupDir, { recursive: true });
    try {
      const files = await fs.readdir(generatedTypesDir);
      for (const file of files) {
        if (file.endsWith('.ts')) {
          const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
          await fs.writeFile(path.join(backupDir, file), content);
        }
      }
    } catch (error) {
      // No existing files to backup
    }
  });

  afterAll(async () => {
    // Restore backup
    try {
      const backupFiles = await fs.readdir(backupDir);
      for (const file of backupFiles) {
        if (file.endsWith('.ts')) {
          const content = await fs.readFile(path.join(backupDir, file), 'utf-8');
          await fs.writeFile(path.join(generatedTypesDir, file), content);
        }
      }
    } catch (error) {
      // No backup to restore
    }
    // Clean up backup
    await fs.rm(backupDir, { recursive: true, force: true });
  });

  describe('OpenAPI Spec Generation', () => {
    it('should have a valid committed OpenAPI spec', async () => {
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      expect(() => JSON.parse(specContent)).not.toThrow();

      const spec = JSON.parse(specContent);
      expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/);
      expect(spec.components).toBeDefined();
      expect(spec.components.schemas).toBeDefined();
    });

    it('should include league schedule route and schema', async () => {
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      const spec = JSON.parse(specContent);

      // Route should exist (controller route extraction)
      expect(spec.paths?.['/leagues/{leagueId}/schedule']).toBeDefined();

      // Schema should exist (DTO scanning)
      const scheduleSchema = spec.components?.schemas?.['LeagueScheduleDTO'];
      expect(scheduleSchema).toBeDefined();

      // Contract requirements: season-aware schedule DTO
      expect(scheduleSchema.required ?? []).toContain('seasonId');
      expect(scheduleSchema.properties?.seasonId).toEqual({ type: 'string' });

      // Races must be typed and use RaceDTO items
      expect(scheduleSchema.required ?? []).toContain('races');
      expect(scheduleSchema.properties?.races?.type).toBe('array');
      expect(scheduleSchema.properties?.races?.items).toEqual({ $ref: '#/components/schemas/RaceDTO' });

      // RaceDTO.date must be ISO-safe string (OpenAPI generator maps Date->date-time, but DTO uses string)
      const raceSchema = spec.components?.schemas?.['RaceDTO'];
      expect(raceSchema).toBeDefined();
      expect(raceSchema.properties?.date).toEqual({ type: 'string' });
    });

    it('should not have duplicate schema names with different casing', async () => {
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      const spec = JSON.parse(specContent);
      const schemas = Object.keys(spec.components.schemas);

      // Check for duplicates with different casing
      const lowerCaseMap = new Map<string, string[]>();
      schemas.forEach(schema => {
        const lower = schema.toLowerCase();
        if (!lowerCaseMap.has(lower)) {
          lowerCaseMap.set(lower, []);
        }
        lowerCaseMap.get(lower)!.push(schema);
      });

      const duplicates = Array.from(lowerCaseMap.entries())
        .filter(([_, names]) => names.length > 1);

      expect(duplicates.length).toBe(0);
    });

    it('should generate spec with consistent naming', async () => {
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      const spec = JSON.parse(specContent);
      const schemas = Object.keys(spec.components.schemas);

      // All schemas should follow DTO naming convention
      const invalidNames = schemas.filter(name => !name.endsWith('DTO') && !name.endsWith('Dto'));
      expect(invalidNames.length).toBe(0);
    });
  });

  describe('Type Generation', () => {
    it('should stamp generated output with the committed OpenAPI SHA256', async () => {
      execSync('npm run api:generate-types', {
        cwd: path.join(__dirname, '../..'),
        stdio: 'pipe',
      });

      const expectedHash = await sha256OfFile(openapiPath);

      const barrelPath = path.join(generatedTypesDir, 'index.ts');
      const barrelContent = await fs.readFile(barrelPath, 'utf-8');

      expect(barrelContent).toContain(`Spec SHA256: ${expectedHash}`);
      expect(barrelContent).toContain(`export type { RaceDTO } from './RaceDTO';`);
      expect(barrelContent).toContain(`export type { DriverDTO } from './DriverDTO';`);

      const sampleDtoPath = path.join(generatedTypesDir, 'RaceDTO.ts');
      const sampleDtoContent = await fs.readFile(sampleDtoPath, 'utf-8');
      expect(sampleDtoContent).toContain(`Spec SHA256: ${expectedHash}`);
    });

    it('should generate TypeScript files for all schemas', async () => {
      // Generate types
      execSync('npm run api:generate-types', {
        cwd: path.join(__dirname, '../..'),
        stdio: 'pipe'
      });

      // Read generated files
      const generatedFiles = await fs.readdir(generatedTypesDir);
      const generatedDTOs = generatedFiles
        .filter(f => f.endsWith('.ts'))
        .map(f => f.replace('.ts', ''));

      // Read OpenAPI spec
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      const spec = JSON.parse(specContent);
      const schemas = Object.keys(spec.components.schemas);

      // Most schemas should have corresponding generated files
      // (allowing for some duplicates/conflicts that are intentionally skipped)
      const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema));

      // Should have at least 95% coverage
      const coverage = (schemas.length - missingFiles.length) / schemas.length;
      expect(coverage).toBeGreaterThan(0.95);
    });

    it('should generate files with correct interface names', async () => {
      const files = await fs.readdir(generatedTypesDir);
      const dtos = files.filter(f => f.endsWith('.ts') && f !== 'index.ts');

      for (const file of dtos) {
        const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');

        // File should contain an interface (name might be normalized)
        expect(content).toMatch(/export interface \w+\s*{/);

        // Should not have duplicate interface names in the same file
        const interfaceMatches = content.match(/export interface (\w+)/g);
        expect(interfaceMatches?.length).toBe(1);
      }
    });

    it('should generate valid TypeScript syntax', async () => {
      const files = await fs.readdir(generatedTypesDir);
      const tsFiles = files.filter(f => f.endsWith('.ts'));

      for (const file of tsFiles) {
        const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');

        if (file === 'index.ts') {
          expect(content).toContain('Auto-generated barrel');
          expect(content).toContain('export type { RaceDTO } from');
          continue;
        }

        // Basic syntax checks
        expect(content).toContain('export interface');
        expect(content).toContain('{');
        expect(content).toContain('}');
        expect(content).toContain('Auto-generated DTO');

        // Should not have syntax errors
        expect(content).not.toMatch(/interface\s+\w+\s*\{\s*\}/); // Empty interfaces
        expect(content).not.toContain('undefined;');
        expect(content).not.toContain('any;');
      }
    });

    it('should handle dependencies correctly', async () => {
      const files = await fs.readdir(generatedTypesDir);
      const dtos = files.filter(f => f.endsWith('.ts'));

      for (const file of dtos) {
        const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
        const importMatches = content.match(/import type \{ (\w+) \} from '\.\/(\w+)';/g) || [];
        
        for (const importLine of importMatches) {
          const match = importLine.match(/import type \{ (\w+) \} from '\.\/(\w+)';/);
          if (match) {
            const [, importedType, fromFile] = match;
            
            // Import type should match the file name
            expect(importedType).toBe(fromFile);
            
            // The imported file should exist
            const importedPath = path.join(generatedTypesDir, `${fromFile}.ts`);
            const exists = await fs.access(importedPath).then(() => true).catch(() => false);
            expect(exists).toBe(true);
          }
        }
      }
    });

    it('should maintain consistent naming between OpenAPI and generated files', async () => {
      const specContent = await fs.readFile(openapiPath, 'utf-8');
      const spec = JSON.parse(specContent);
      const schemas = Object.keys(spec.components.schemas);

      const generatedFiles = await fs.readdir(generatedTypesDir);
      const generatedDTOs = generatedFiles
        .filter(f => f.endsWith('.ts'))
        .map(f => f.replace('.ts', ''));

      // Check that most schemas have matching files (allowing for some edge cases)
      const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema));
      const coverage = (schemas.length - missingFiles.length) / schemas.length;
      expect(coverage).toBeGreaterThan(0.95);

      // Check that most files have matching schemas (allowing for normalization)
      const extraFiles = generatedDTOs.filter(dto => !schemas.includes(dto));
      const extraCoverage = (generatedDTOs.length - extraFiles.length) / generatedDTOs.length;
      expect(extraCoverage).toBeGreaterThan(0.95);
    });
  });

  describe('Integration', () => {
    it('should generate types that can be imported without errors', async () => {
      // Generate types first
      execSync('npm run api:generate-types', { 
        cwd: path.join(__dirname, '../..'),
        stdio: 'pipe'
      });

      // Try to import a few key DTOs
      const testDTOs = [
        'RaceDTO',
        'DriverDTO',
        'RequestAvatarGenerationInputDTO',
        'RequestAvatarGenerationOutputDTO'
      ];

      for (const dto of testDTOs) {
        const filePath = path.join(generatedTypesDir, `${dto}.ts`);
        const exists = await fs.access(filePath).then(() => true).catch(() => false);
        
        if (exists) {
          const content = await fs.readFile(filePath, 'utf-8');
          // Should be valid TypeScript that can be parsed
          expect(content).toContain(`export interface ${dto}`);
        }
      }
    });

    it('should handle the full generation workflow', async () => {
      // Run complete workflow
      execSync('npm run api:sync-types', { 
        cwd: path.join(__dirname, '../..'),
        stdio: 'pipe'
      });

      // Verify both spec and types were generated
      const specExists = await fs.access(openapiPath).then(() => true).catch(() => false);
      expect(specExists).toBe(true);

      const files = await fs.readdir(generatedTypesDir);
      const tsFiles = files.filter(f => f.endsWith('.ts'));
      expect(tsFiles.length).toBeGreaterThan(0);
    });
  });
});