import 'dart:io';

import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_catalog.dart';
import 'package:oto/oto/commands/command_registry.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';

void main() {
  setUpAll(() {
    registerAllCommands();
  });

  test('all command types are registered', () {
    final registered = Command.registeredTypes.toSet();
    final missing =
        CommandType.values.where((t) => !registered.contains(t)).toList();
    expect(
      missing,
      isEmpty,
      reason: 'CommandType values without a Command.register() call: $missing. '
          'Add registration in the relevant register*() function and call it from registerAllCommands().',
    );
  });

  test('all registered commands expose specs', () {
    final specs = Command.specs;
    final missing =
        Command.registeredTypes.where((t) => !specs.containsKey(t)).toList();
    expect(
      missing,
      isEmpty,
      reason: 'Registered commands without a CommandSpec: $missing. '
          'Pass spec: CommandSpec(...) to Command.register().',
    );
  });

  test('spec sample paths exist when provided', () {
    final missing = <String>[];
    for (final entry in Command.specs.entries) {
      final sample = entry.value.samplePath;
      if (sample == null) continue;
      if (!File(sample).existsSync()) {
        missing.add('${entry.key} -> $sample');
      }
    }
    expect(
      missing,
      isEmpty,
      reason: 'CommandSpec.samplePath must point to an existing file. '
          'Missing: $missing',
    );
  });

  test('spec sample paths contain the registered command', () {
    final mismatches = <String>[];
    for (final entry in Command.specs.entries) {
      final sample = entry.value.samplePath;
      if (sample == null) continue;
      final file = File(sample);
      if (!file.existsSync()) continue;
      final commandName = entry.key.name;
      final pattern =
          RegExp(r'^\s*-\s*command:\s*' + RegExp.escape(commandName) + r'\s*$');
      final hasCommand = file.readAsLinesSync().any((line) {
        final trimmed = line.trimLeft();
        if (trimmed.startsWith('#')) return false;
        return pattern.hasMatch(line);
      });
      if (!hasCommand) {
        mismatches.add('${entry.key} -> $sample');
      }
    }
    expect(
      mismatches,
      isEmpty,
      reason: 'CommandSpec.samplePath must reference a sample that actually '
          'includes a non-commented `- command: <Type>` line. Mismatches: '
          '$mismatches',
    );
  });

  test('spec category and dataModel are non-empty', () {
    final bad = <String>[];
    for (final entry in Command.specs.entries) {
      final spec = entry.value;
      if (spec.category.isEmpty || spec.dataModel.isEmpty) {
        bad.add('${entry.key}');
      }
    }
    expect(bad, isEmpty,
        reason: 'CommandSpec.category and dataModel must be non-empty: $bad');
  });

  // REVIEW_SAMPLE-3: stale iOS field name regression tests
  // These tests guard against stale/old field names that no longer match
  // the actual data models (DataBuildiOS, DataArchiveiOS, DataExportiOS,
  // DataTestflightUpload, DataTestflightStatusCheck, DataTestflightDistribute).

  // Stale field names that should NOT appear in any sample YAML:
  // - build_ios.yaml: `workspacePath` was replaced with `xcworkspaceFilePath`/`xcodeProjectFilePath`
  // - build_ios.yaml: `exportOptionsPlist` was replaced with `exportOptionPlistPath`
  // - build_ios.yaml: `username`/`password` were replaced with `apiKeyPath`/`appID` for TestFlight

  test('iOS sample must not contain stale field: workspacePath', () {
    const samplePath = 'assets/yaml/sample/10_build_ios.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    bool hasStaleWorkspacePath = false;
    for (final line in content.split('\n')) {
      if (line.trimLeft().startsWith('#')) continue;
      if (RegExp(r'^[ \t]+workspacePath\s*:').hasMatch(line)) {
        hasStaleWorkspacePath = true;
        break;
      }
    }
    expect(hasStaleWorkspacePath, isFalse,
        reason: '10_build_ios.yaml contains stale field name `workspacePath`. '
            'Use `xcworkspaceFilePath` or `xcodeProjectFilePath` to match DataBuildiOS/DataArchiveiOS.');
  });

  test('iOS sample must not contain stale field: exportOptionsPlist', () {
    const samplePath = 'assets/yaml/sample/10_build_ios.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final lines = content.split('\n');
    bool hasStaleExportOptionsPlist = false;
    for (final line in lines) {
      if (line.trimLeft().startsWith('#')) continue;
      if (RegExp(r'^[ \t]+exportOptionsPlist\s*:').hasMatch(line)) {
        hasStaleExportOptionsPlist = true;
        break;
      }
    }
    expect(hasStaleExportOptionsPlist, isFalse,
        reason:
            '10_build_ios.yaml contains stale field name `exportOptionsPlist`. '
            'Use `exportOptionPlistPath` to match DataExportiOS.');
  });

  test('iOS TestFlight samples must not contain stale field: username/password',
      () {
    const samplePath = 'assets/yaml/sample/10_build_ios.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final lines = content.split('\n');

    // Check for stale `username:` or `password:` YAML keys (comment lines excluded)
    bool hasStaleUser = false;
    bool hasStalePass = false;
    for (final line in lines) {
      if (line.trimLeft().startsWith('#')) continue;
      if (RegExp(r'^[ \t]+username\s*:').hasMatch(line)) {
        hasStaleUser = true;
        break;
      }
      if (RegExp(r'^[ \t]+password\s*:').hasMatch(line)) {
        hasStalePass = true;
        break;
      }
    }

    expect(
      hasStaleUser || hasStalePass,
      isFalse,
      reason:
          '10_build_ios.yaml contains stale TestFlight field names `username`/`password`. '
          'DataTestflightUpload expects `apiKeyPath` and `ipaPath`. '
          'DataTestflightStatusCheck expects `appID`, `apiKeyPath`, `version`, `buildNumber`. '
          'DataTestflightDistribute expects `apiKeyPath`, `appID`. '
          'Use `apiKeyPath` instead of `username`/`password`.',
    );
  });

  test('sample fields must include expected iOS model fields', () {
    const samplePath = 'assets/yaml/sample/10_build_ios.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();

    // DataBuildiOS field names that should be present:
    //   scheme, configuration, xcworkspaceFilePath, xcodeProjectFilePath
    // DataArchiveiOS field names that should be present:
    //   xcworkspaceFilePath, xcodeProjectFilePath, scheme, archivePath
    // DataExportiOS field names that should be present:
    //   archivePath, exportOptionPlistPath, exportPath

    final expectedFields = [
      'xcworkspaceFilePath', // DataBuildiOS, DataArchiveiOS
      'scheme', // DataBuildiOS, DataArchiveiOS
      'configuration', // DataBuildiOS, DataArchiveiOS
      'archivePath', // DataArchiveiOS, DataExportiOS
      'exportOptionPlistPath', // DataExportiOS
      'exportPath', // DataExportiOS
      'apiKeyPath', // DataTestflightUpload, DataTestflightStatusCheck, DataTestflightDistribute
      'appID', // DataTestflightStatusCheck, DataTestflightDistribute
    ];

    final missingFields = <String>[];
    for (final field in expectedFields) {
      // Look for field as a YAML key: optional whitespace, then the field name
      // followed by colon, not inside a comment
      final pattern = RegExp(
        r'^[ \t]+' + RegExp.escape(field) + r'\s*:',
      );
      final lines = content.split('\n');
      bool found = false;
      for (final line in lines) {
        if (line.trimLeft().startsWith('#')) continue;
        if (pattern.hasMatch(line)) {
          found = true;
          break;
        }
      }
      if (!found) {
        missingFields.add(field);
      }
    }

    expect(missingFields, isEmpty,
        reason:
            '10_build_ios.yaml should contain fields matching actual data models. '
            'Missing fields: $missingFields');
  });

  test('all registered commands have samplePath (with allowlist)', () {
    // These commands intentionally have no sample yet:
    // - SimpleCommand: Internal utility base class, not user-facing
    // - CreateAppData: Internal app data generation
    // - BuildDartCompile: Internal build helper
    // - SMBAuth: Windows-only infra command
    final allowlistedWithoutSample = {
      'SimpleCommand',
      'CreateAppData',
      'BuildDartCompile',
      'SMBAuth',
    };

    final missingSample = <String>[];
    for (final entry in Command.specs.entries) {
      final sample = entry.value.samplePath;
      final typeName = entry.key.name;
      if (sample == null) {
        if (!allowlistedWithoutSample.contains(typeName)) {
          missingSample.add('${entry.key} (no samplePath, not in allowlist)');
        }
      }
    }
    expect(missingSample, isEmpty,
        reason:
            'Registered public commands without samplePath must be explicitly allowlisted. '
            'Missing: $missingSample. '
            'If a command was intentionally not allowlisted, add a sample file and '
            'specify samplePath in the CommandSpec.');
  });

  test(
      'all non-allowlisted commands have sample files containing the registered command',
      () {
    final allowlistedWithoutSample = {
      'SimpleCommand',
      'CreateAppData',
      'BuildDartCompile',
      'SMBAuth',
    };

    final missingContent = <String>[];
    for (final entry in Command.specs.entries) {
      final sample = entry.value.samplePath;
      if (sample == null) continue;
      final typeName = entry.key.name;
      if (allowlistedWithoutSample.contains(typeName)) continue;
      final file = File(sample);
      if (!file.existsSync()) continue;
      final pattern = RegExp(
          r'^\s*-\s*command:\s*' + RegExp.escape(entry.key.name) + r'\s*$');
      final hasCommand = file.readAsLinesSync().any((line) {
        final trimmed = line.trimLeft();
        if (trimmed.startsWith('#')) return false;
        return pattern.hasMatch(line);
      });
      if (!hasCommand) {
        missingContent.add('${entry.key} -> $sample');
      }
    }
    expect(missingContent, isEmpty,
        reason:
            'All non-allowlisted samplePath files must contain at least one non-commented `- command: <Type>` line. '
            'Missing: $missingContent');
  });

  test(
      'GitHub PR samples use write tags (<@>) not read tags (<!>) for result storage',
      () {
    const samplePath = 'assets/yaml/sample/09_network.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();

    // Locate GitHub PR command blocks and check their write fields
    final lines = content.split('\n');
    bool inGithubCreate = false;
    bool inGithubList = false;
    for (final line in lines) {
      // Track whether we're inside a GitHub PR command block
      if (line.contains(r'- command: GitHubPullRequestCreate') ||
          line.contains('- command: GitHubPullRequestList')) {
        inGithubCreate = line.contains('GitHubPullRequestCreate');
        inGithubList = !inGithubCreate;
      } else if (line.contains('- command: ') &&
          !line.contains('GitHubPullRequest')) {
        inGithubCreate = false;
        inGithubList = false;
      }

      // In PR create/list blocks, setURL/setNumber/setValue must use write tag <@>, not read tag <!>
      if (inGithubCreate || inGithubList) {
        final trimmed = line.trimLeft();
        if (trimmed.startsWith('setURL:') ||
            trimmed.startsWith('setNumber:') ||
            trimmed.startsWith('setValue:')) {
          // Read tag <!property... is wrong here — must be write tag <@property...>
          if (RegExp(r'<![^>]+>').hasMatch(trimmed)) {
            fail('GitHub PR command write fields must use write tags '
                '(<@property...>) not read tags (<!property...>). '
                'Found: $trimmed');
          }
        }
      }
    }
  });

  test('all samplePath YAML files parse as maps with a commands list', () {
    final parseFailures = <String>[];
    for (final entry in Command.specs.entries) {
      final sample = entry.value.samplePath;
      if (sample == null) continue;
      final file = File(sample);
      if (!file.existsSync()) continue;
      try {
        final content = file.readAsStringSync();
        final node = loadYaml(content);
        if (node == null || node is! Map) {
          parseFailures.add('${entry.key} -> $sample: root is not a YAML map');
          continue;
        }
        if (!node.containsKey('commands') || node['commands'] == null) {
          parseFailures.add('${entry.key} -> $sample: missing commands list');
          continue;
        }
        final commands = node['commands'];
        if (commands is! List) {
          parseFailures.add('${entry.key} -> $sample: commands is not a list');
          continue;
        }
        for (var i = 0; i < commands.length; i++) {
          final cmd = commands[i];
          if (cmd is! Map || !cmd.containsKey('command')) {
            parseFailures.add(
                '${entry.key} -> $sample: commands[$i] missing command key');
            break;
          }
        }
      } catch (e) {
        parseFailures.add('${entry.key} -> $sample: YAML parse error: $e');
      }
    }
    expect(parseFailures, isEmpty,
        reason:
            'Sample YAML files must parse as valid maps with commands lists. Failures: $parseFailures');
  });

  test('README examples must not contain stale field workspacePath', () {
    final file = File('README.md');
    if (!file.existsSync()) {
      fail('README.md not found');
    }
    final content = file.readAsStringSync();
    for (final line in content.split('\n')) {
      if (line.trimLeft().startsWith('#')) continue;
      expect(RegExp(r'^[ \t]+workspacePath\s*:').hasMatch(line), isFalse,
          reason: 'README.md contains stale field `workspacePath`. '
              'Use `xcworkspaceFilePath` or `xcodeProjectFilePath` to match DataBuildiOS/DataArchiveiOS.');
    }
  });

  test('AwsCli sample uses sub-command key (not subCommand)', () {
    const samplePath = 'assets/yaml/sample/19_awscli.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final node = loadYaml(content) as Map;
    final commands = node['commands'] as List;
    final awsCliBlock = commands.firstWhere(
      (c) => (c as Map)['command'] == 'AwsCli',
      orElse: () => null,
    );
    expect(awsCliBlock, isNotNull,
        reason: '19_awscli.yaml must contain an AwsCli command block');
    final param = awsCliBlock['param'] as Map;
    expect(param.containsKey('sub-command'), isTrue,
        reason:
            'AwsCli sample must use the key sub-command (not subCommand) to match DataAwsCli @JsonKey(name: "sub-command")');
    expect(param.containsKey('subCommand'), isFalse,
        reason:
            'AwsCli sample must not use the key subCommand; use sub-command instead');
  });

  test('Jira sample includes both id and token fields', () {
    const samplePath = 'assets/yaml/sample/21_jira.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final node = loadYaml(content) as Map;
    final commands = node['commands'] as List;
    final jiraBlock = commands.firstWhere(
      (c) => (c as Map)['command'] == 'Jira',
      orElse: () => null,
    );
    expect(jiraBlock, isNotNull,
        reason: '21_jira.yaml must contain a Jira command block');
    final param = jiraBlock['param'] as Map;
    expect(param.containsKey('id'), isTrue,
        reason:
            'Jira sample must include the id field for Jira domain/account');
    expect(param.containsKey('token'), isTrue,
        reason:
            'Jira sample must include the token field for Jira API authentication');
  });

  test('Protobuf sample commands list contains valid string entries', () {
    const samplePath = 'assets/yaml/sample/18_protobuf.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final node = loadYaml(content) as Map;
    final commands = node['commands'] as List;
    final protoBlock = commands.firstWhere(
      (c) => (c as Map)['command'] == 'Protobuf',
      orElse: () => null,
    );
    expect(protoBlock, isNotNull,
        reason: '18_protobuf.yaml must contain a Protobuf command block');
    final param = protoBlock['param'] as Map;
    expect(param.containsKey('commands'), isTrue,
        reason: 'Protobuf sample must include a commands field');
    final cmdList = param['commands'] as List;
    expect(cmdList.isNotEmpty, isTrue,
        reason: 'Protobuf commands list must not be empty');
    for (var item in cmdList) {
      expect(item is String, isTrue,
          reason:
              'Protobuf commands list must contain string entries. Got: $item (${item.runtimeType})');
    }
  });

  test('BuildFlutter sample uses supported fields and flutter targets', () {
    const samplePath = 'assets/yaml/sample/11_build_flutter.yaml';
    final file = File(samplePath);
    if (!file.existsSync()) {
      fail('Sample file not found: $samplePath');
    }
    final content = file.readAsStringSync();
    final node = loadYaml(content) as Map;
    final commands = node['commands'] as List;
    final buildFlutterBlocks =
        commands.where((c) => (c as Map)['command'] == 'BuildFlutter').toList();

    expect(buildFlutterBlocks.isNotEmpty, isTrue,
        reason:
            '11_build_flutter.yaml must contain BuildFlutter command blocks');

    final allowedPlatforms = {
      'ios',
      'apk',
      'appbundle',
      'web',
      'macos',
      'windows',
      'linux'
    };

    for (var block in buildFlutterBlocks) {
      final param = (block as Map)['param'] as Map;
      expect(param.containsKey('targetPath'), isFalse,
          reason:
              'BuildFlutter sample must not use targetPath (stale field). Use workspace instead.');
      expect(param.containsKey('workspace'), isTrue,
          reason: 'BuildFlutter sample must define workspace.');
      expect(param.containsKey('platform'), isTrue,
          reason: 'BuildFlutter sample must define platform.');

      final platform = param['platform']?.toString().toLowerCase();
      expect(allowedPlatforms.contains(platform), isTrue,
          reason:
              'BuildFlutter platform must be one of $allowedPlatforms. Got: $platform');
    }
  });

  test('command catalog contract exposes stable fields', () {
    final entries = const CommandCatalog().entries();
    expect(entries, isNotEmpty);

    final firstJson = entries.first.toJson();
    expect(firstJson.keys, containsAll([
      'command',
      'category',
      'dataModel',
      'samplePath',
      'hasSample',
      'sampleExists',
    ]));
    expect(firstJson['command'], isA<String>());
    expect(firstJson['category'], isA<String>());
    expect(firstJson['dataModel'], isA<String>());
    expect(firstJson['hasSample'], isA<bool>());
    expect(firstJson['sampleExists'], isA<bool>());
  });

  test('command catalog filters category and command case-insensitively', () {
    final catalog = const CommandCatalog();
    final buildEntries = catalog.entries(category: 'BUILD');
    expect(buildEntries, isNotEmpty);
    expect(buildEntries.every((entry) => entry.category == 'build'), isTrue);

    final printEntries = catalog.entries(command: 'print');
    expect(printEntries.map((entry) => entry.command), ['Print']);
  });
}
