// generator_test.dart — Pillar 3: Generator test suite with golden files
// Run: dart test test/generator_test.dart

import 'dart:io';
import 'package:test/test.dart';
import '../src/resolver.dart';
import '../src/renderer.dart';
import '../src/validator.dart';
import '../src/models.dart';

// ─── Test fixtures ─────────────────────────────────────────────────────────

final _blocCleanBrief = ProjectBrief(
  projectName: 'TestApp',
  packageId: 'com.test.testapp',
  description: 'Test app for golden tests',
  scale: 'medium',
  stateManagement: 'bloc',
  routing: 'gorouter',
  architecture: 'clean',
  backends: ['firebase'],
  auth: 'firebase_auth',
  platforms: ['ios', 'android'],
  codegenTools: ['freezed'],
  flavors: ['dev', 'prod'],
  cicd: 'github_actions',
  testingDepth: 'unit_widget',
  e2eTool: 'patrol',
  designSource: 'none',
  figmaUrl: '',
  apiDocsFormat: 'none',
  apiDocsPath: '',
  referenceRepos: [],
  localPaths: [],
  featureModules: ['auth', 'home', 'products'],
  specialFeatures: [],
  i18nEnabled: false,
  locales: ['en'],
);

final _riverpodFFBrief = ProjectBrief(
  projectName: 'TaskFlow',
  packageId: 'com.test.taskflow',
  description: 'Task management app',
  scale: 'small',
  stateManagement: 'riverpod',
  routing: 'gorouter',
  architecture: 'feature_first',
  backends: ['supabase'],
  auth: 'supabase_auth',
  platforms: ['ios', 'android', 'web'],
  codegenTools: ['freezed', 'json_serializable'],
  flavors: ['dev', 'prod'],
  cicd: 'github_actions',
  testingDepth: 'unit_widget',
  e2eTool: 'patrol',
  designSource: 'none',
  figmaUrl: '',
  apiDocsFormat: 'none',
  apiDocsPath: '',
  referenceRepos: [],
  localPaths: [],
  featureModules: ['auth', 'tasks', 'profile'],
  specialFeatures: [],
  i18nEnabled: true,
  locales: ['en', 'fr'],
);

final _getxMvcBrief = ProjectBrief(
  projectName: 'LegacyApp',
  packageId: 'com.test.legacy',
  description: 'Legacy GetX app',
  scale: 'medium',
  stateManagement: 'getx',
  routing: 'getx_nav',
  architecture: 'mvc',
  backends: ['rest'],
  auth: 'jwt_rest',
  platforms: ['ios', 'android'],
  codegenTools: [],
  flavors: ['dev', 'prod'],
  cicd: 'codemagic',
  testingDepth: 'unit_widget',
  e2eTool: 'patrol',
  designSource: 'none',
  figmaUrl: '',
  apiDocsFormat: 'openapi',
  apiDocsPath: 'docs/api.yaml',
  referenceRepos: [],
  localPaths: [],
  featureModules: ['auth', 'dashboard'],
  specialFeatures: [],
  i18nEnabled: false,
  locales: ['en'],
);

void main() {
// ─── Resolver tests ─────────────────────────────────────────────────────────

  group('Resolver', () {
    test('BLoC + Clean resolves correct files', () {
      final files = Resolver.resolve(_blocCleanBrief);

      // Universal — always present
      expect(files, contains('rules/universal/flutter-core'));
      expect(files, contains('rules/universal/ui-ux-standards'));
      expect(files, contains('rules/universal/project-context'));

      // Security — always present (Pillar 5)
      expect(files, contains('rules/security/security-standards'));

      // Error handling — always present
      expect(files, contains('rules/error-handling/error-handling'));

      // Stack-specific
      expect(files, contains('rules/state-management/bloc'));
      expect(files, contains('rules/architecture/clean'));
      expect(files, contains('rules/routing/gorouter'));
      expect(files, contains('rules/backend/firebase'));

      // Testing — SM-matched
      expect(files, contains('rules/testing/testing-bloc'));

      // Platforms (Pillar 4)
      expect(files, contains('rules/platform/platform-ios'));
      expect(files, contains('rules/platform/platform-android'));
      expect(files, containsNot('rules/platform/platform-web'));

      // Codegen (Pillar 4)
      expect(files, contains('rules/codegen/codegen-freezed'));
      expect(files, containsNot('rules/codegen/codegen-injectable'));

      // Agents
      expect(files, contains('agents/code-reviewer'));
      expect(files, containsNot('agents/migration-agent')); // not GetX
      expect(files,
          contains('agents/security-agent')); // firebase_auth triggers it

      // NOT included
      expect(files, containsNot('rules/state-management/riverpod'));
      expect(files, containsNot('rules/state-management/getx'));
      expect(files, containsNot('rules/architecture/feature_first'));
      expect(files, containsNot('rules/routing/getx_nav'));
    });

    test('Riverpod + Feature-First + Web resolves web platform template', () {
      final files = Resolver.resolve(_riverpodFFBrief);
      expect(files, contains('rules/platform/platform-web'));
      expect(files, contains('rules/state-management/riverpod'));
      expect(files, contains('rules/architecture/feature_first'));
      expect(files, contains('rules/codegen/codegen-freezed'));
      expect(files, contains('rules/codegen/codegen-json_serializable'));
      expect(files, contains('rules/i18n/localization')); // i18nEnabled: true
      expect(files, containsNot('agents/migration-agent'));
    });

    test('GetX + MVC includes migration-agent', () {
      final files = Resolver.resolve(_getxMvcBrief);
      expect(files, contains('agents/migration-agent'));
      expect(files, contains('rules/state-management/getx'));
      expect(files, contains('rules/architecture/mvc'));
      expect(files, contains('agents/api-client-gen')); // rest backend
      expect(files,
          contains('skills/generate-api-client')); // apiDocsFormat != none
      // Hooks are emitted only when stack.codegen is non-empty
      expect(files, containsNot('hooks/hooks-json'));
      expect(files, containsNot('hooks/flutter-analyze'));
    });

    test('Codegen stack includes Cursor hooks templates', () {
      final files = Resolver.resolve(_blocCleanBrief);
      expect(files, contains('hooks/hooks-json'));
      expect(files, contains('hooks/arch-guard'));
    });

    test('Realtime special feature includes realtime template', () {
      final brief = ProjectBrief(
        projectName: 'RealtimeApp',
        packageId: 'com.test.rt',
        description: '',
        scale: 'medium',
        stateManagement: 'riverpod',
        routing: 'gorouter',
        architecture: 'feature_first',
        backends: ['supabase'],
        auth: 'supabase_auth',
        platforms: ['ios'],
        codegenTools: [],
        flavors: ['dev'],
        cicd: 'github_actions',
        testingDepth: 'unit_widget',
        e2eTool: 'patrol',
        designSource: 'none',
        figmaUrl: '',
        apiDocsFormat: 'none',
        apiDocsPath: '',
        referenceRepos: [],
        localPaths: [],
        featureModules: [],
        specialFeatures: ['realtime'],
        i18nEnabled: false,
        locales: ['en'],
      );
      final files = Resolver.resolve(brief);
      expect(files, contains('rules/backend/realtime'));
    });

    test('E2E testing depth includes e2e template', () {
      final brief = ProjectBrief(
        projectName: 'E2EApp',
        packageId: 'com.test.e2e',
        description: '',
        scale: 'small',
        stateManagement: 'bloc',
        routing: 'gorouter',
        architecture: 'clean',
        backends: ['rest'],
        auth: 'none',
        platforms: ['ios'],
        codegenTools: [],
        flavors: ['dev'],
        cicd: 'github_actions',
        testingDepth: 'full',
        e2eTool: 'patrol',
        designSource: 'none',
        figmaUrl: '',
        apiDocsFormat: 'none',
        apiDocsPath: '',
        referenceRepos: [],
        localPaths: [],
        featureModules: [],
        specialFeatures: [],
        i18nEnabled: false,
        locales: ['en'],
      );
      final files = Resolver.resolve(brief);
      expect(files, contains('rules/testing/testing-e2e-patrol'));
    });

    test('No duplicate files in resolved list', () {
      final files = Resolver.resolve(_blocCleanBrief);
      final unique = files.toSet();
      expect(files.length, equals(unique.length),
          reason: 'Duplicate template files detected');
    });
  });

// ─── Renderer / placeholder tests ───────────────────────────────────────────

  group('Renderer — placeholder substitution', () {
    test('buildContext produces all required keys', () {
      // Access via Renderer._buildContext (white-box test)
      // Instead, verify rendered output has no unreplaced {{VAR}} patterns
      // by checking a known template snippet
      final context = {
        'PROJECT_NAME': 'TestApp',
        'PACKAGE_ID': 'com.test.testapp',
      };
      final template = 'Project: {{PROJECT_NAME}} ({{PACKAGE_ID}})';
      final result = template
          .replaceAll('{{PROJECT_NAME}}', context['PROJECT_NAME']!)
          .replaceAll('{{PACKAGE_ID}}', context['PACKAGE_ID']!);
      expect(result, equals('Project: TestApp (com.test.testapp)'));
      expect(result, isNot(contains('{{')));
    });

    test('Rendered output has no unreplaced {{PLACEHOLDER}} patterns',
        () async {
      final templateDir = _templateDir();
      if (!Directory(templateDir).existsSync()) {
        markTestSkipped(
            'Template directory not found at $templateDir — run from generator/');
        return;
      }

      final rendered = await Renderer.render(
        brief: _blocCleanBrief,
        templateFiles: Resolver.resolve(_blocCleanBrief),
        templateSrc: templateDir,
      );

      final unresolved = <String>[];
      for (final entry in rendered.entries) {
        final matches = RegExp(r'\{\{[A-Z_]+\}\}').allMatches(entry.value);
        for (final m in matches) {
          unresolved.add('${entry.key}: ${m.group(0)}');
        }
      }
      expect(unresolved, isEmpty,
          reason: 'Unreplaced placeholders:\n${unresolved.join("\n")}');
    });

    test('hooks/arch-guard renders real template content, not placeholder',
        () async {
      final templateDir = _templateDir();
      if (!Directory(templateDir).existsSync()) {
        markTestSkipped(
            'Template directory not found at $templateDir — run from generator/');
        return;
      }

      final rendered = await Renderer.render(
        brief: _blocCleanBrief,
        templateFiles: ['hooks/arch-guard'],
        templateSrc: templateDir,
      );

      final content = rendered['hooks/arch-guard.ts'];
      expect(content, isNotNull, reason: 'arch-guard.ts must be produced');
      expect(content, isNot(contains('Template not found')),
          reason:
              'Default template dir should resolve real arch-guard.ts.tmpl');
      expect(content, contains('arch-guard'));
    });
  });

// ─── Validator tests ─────────────────────────────────────────────────────────

  group('Validator', () {
    test('Valid brief passes validation', () async {
      final result = await Validator.validate(_blocCleanBrief);
      expect(result.isValid, isTrue);
      expect(result.errors, isEmpty);
    });

    test('Invalid state_management fails validation', () async {
      final brief = ProjectBrief(
        projectName: 'X',
        packageId: 'com.x.x',
        description: '',
        scale: 'small',
        stateManagement: 'invalid_sm',
        routing: 'gorouter',
        architecture: 'clean',
        backends: ['rest'],
        auth: 'none',
        platforms: ['ios'],
        codegenTools: [],
        flavors: ['dev'],
        cicd: 'github_actions',
        testingDepth: 'unit_widget',
        e2eTool: 'patrol',
        designSource: 'none',
        figmaUrl: '',
        apiDocsFormat: 'none',
        apiDocsPath: '',
        referenceRepos: [],
        localPaths: [],
        featureModules: [],
        specialFeatures: [],
        i18nEnabled: false,
        locales: ['en'],
      );
      final result = await Validator.validate(brief);
      expect(result.isValid, isFalse);
      expect(result.errors, anyElement(contains('invalid_sm')));
    });

    test('Missing project name fails validation', () async {
      final brief = ProjectBrief(
        projectName: '',
        packageId: 'com.x.x',
        description: '',
        scale: 'small',
        stateManagement: 'riverpod',
        routing: 'gorouter',
        architecture: 'clean',
        backends: ['rest'],
        auth: 'none',
        platforms: ['ios'],
        codegenTools: [],
        flavors: ['dev'],
        cicd: 'github_actions',
        testingDepth: 'unit_widget',
        e2eTool: 'patrol',
        designSource: 'none',
        figmaUrl: '',
        apiDocsFormat: 'none',
        apiDocsPath: '',
        referenceRepos: [],
        localPaths: [],
        featureModules: [],
        specialFeatures: [],
        i18nEnabled: false,
        locales: ['en'],
      );
      final result = await Validator.validate(brief);
      expect(result.isValid, isFalse);
      expect(result.errors, anyElement(contains('project.name')));
    });
  });

// ─── Golden file tests (Pillar 3) ───────────────────────────────────────────

  group('Golden file tests', () {
    test('BLoC + Clean renders match golden files', () async {
      final templateDir = _templateDir();
      if (!Directory(templateDir).existsSync()) {
        markTestSkipped('Template dir not found');
        return;
      }

      final rendered = await Renderer.render(
        brief: _blocCleanBrief,
        templateFiles: Resolver.resolve(_blocCleanBrief),
        templateSrc: templateDir,
      );

      for (final entry in rendered.entries) {
        final goldenPath = 'test/golden/bloc-clean-firebase/${entry.key}';
        final goldenFile = File(goldenPath);
        if (!goldenFile.existsSync()) {
          // Create golden file on first run
          await goldenFile.parent.create(recursive: true);
          await goldenFile.writeAsString(entry.value);
          print('Created golden: $goldenPath');
        } else {
          final golden = await goldenFile.readAsString();
          expect(entry.value, equals(golden),
              reason: 'Golden mismatch for ${entry.key}.\n'
                  'Update goldens: dart test --update-goldens');
        }
      }
    });

    test('Riverpod + FF renders match golden files', () async {
      final templateDir = _templateDir();
      if (!Directory(templateDir).existsSync()) {
        markTestSkipped('Template dir not found');
        return;
      }
      final rendered = await Renderer.render(
        brief: _riverpodFFBrief,
        templateFiles: Resolver.resolve(_riverpodFFBrief),
        templateSrc: templateDir,
      );
      _compareGoldens('test/golden/riverpod-ff-supabase', rendered);
    });

    test('GetX + MVC renders match golden files', () async {
      final templateDir = _templateDir();
      if (!Directory(templateDir).existsSync()) {
        markTestSkipped('Template dir not found');
        return;
      }
      final rendered = await Renderer.render(
        brief: _getxMvcBrief,
        templateFiles: Resolver.resolve(_getxMvcBrief),
        templateSrc: templateDir,
      );
      _compareGoldens('test/golden/getx-mvc-rest', rendered);
    });
  });
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

Future<void> _compareGoldens(
    String goldenDir, Map<String, String> rendered) async {
  for (final entry in rendered.entries) {
    final goldenFile = File('$goldenDir/${entry.key}');
    if (!goldenFile.existsSync()) {
      await goldenFile.parent.create(recursive: true);
      await goldenFile.writeAsString(entry.value);
    } else {
      expect(entry.value, await goldenFile.readAsString(),
          reason: 'Golden mismatch: $goldenDir/${entry.key}');
    }
  }
}

String _templateDir() {
  // Walk up from CWD until we find the sibling templates/ directory.
  // Works under `dart test` (snapshot CWD) and direct script execution alike.
  var dir = Directory.current;
  for (var i = 0; i < 5; i++) {
    final candidate = Directory('${dir.path}/../templates');
    if (candidate.existsSync()) return candidate.path;
    final inside = Directory('${dir.path}/templates');
    if (inside.existsSync()) return inside.path;
    if (dir.parent.path == dir.path) break;
    dir = dir.parent;
  }
  return '${Directory.current.path}/../templates';
}

Matcher containsNot(dynamic expected) => isNot(contains(expected));
