import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:oto/cli/commands/command_exe.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/build_result.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:test/test.dart';

void main() {
  setUp(() {
    Application.instance.property = {};
    Application.instance.commandStates = {};
    Application.instance.dataCommandMap = {};
  });

  test('build returns failure instead of exiting on invalid yaml', () async {
    const invalidYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: doesNotExist
''';

    final result = await Application.instance
        .build(BuildType.file, yamlContent: invalidYaml);

    expect(result.success, isFalse);
    expect(result.exitCode, 10);
    expect(result.error, isNotNull);
  });

  test('file build returns success for print-only pipeline', () async {
    const yaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);

    expect(result.success, isTrue);
    expect(result.exitCode, 0);
  });

  test('file build without property workspace uses local workspace without Jenkins env',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

    final result = await Application.instance.build(
      BuildType.file,
      yamlContent: yaml,
      logEnable: false,
    );

    expect(result.success, isTrue);
    expect(Application.instance.property['workspace'], isNotNull);
    expect(Application.instance.property['workspace'],
        Directory.current.path.replaceAll('\\', '/'));
  });

  test('file build can run twice in same process', () async {
    const yaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

    final first =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    final second =
        await Application.instance.build(BuildType.file, yamlContent: yaml);

    expect(first.success, isTrue);
    expect(second.success, isTrue);
  });

  test('build fails with message when YAML root is not a map', () async {
    const yaml = '''
- command: Print
  id: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), contains('Build YAML root must be a map'));
  });

  test('build fails with message when property is not a map', () async {
    const yaml = '''
property:
  - bad
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), contains('Validate build yaml'));
  });

  test('build fails with message when command id is not a string', () async {
    const yaml = '''
commands:
  - command: Print
    id: 123
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), contains('Validate command list'));
  });

  test('build fails with message when command type is not a string', () async {
    const yaml = '''
commands:
  - command: 999
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), contains('Validate command list'));
  });

  test('build fails with validation message when workflow key is not a string',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - 123: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(),
        isNot(contains('Converting object to an encodable object failed')));
    expect(result.error.toString(), contains('workflow[0]'));
  });

  test('build fails with validation message when exe-handle branch is missing',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe-handle:
        id: hello
        on-fail:
          - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(),
        contains('exe-handle syntax requires on-success'));
  });

  // REVIEW_VALIDATE-1: exe command id type guard
  test(
      'build fails with validation message when exe command id is not a string',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: 123
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('is not a subtype')));
    expect(result.error.toString(), contains('exe'));
  });

  // REVIEW_VALIDATE-1: wait-until-string condition type guard
  test(
      'build fails with validation message when wait-until-string value is not a string',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - wait-until-string: 123
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('is not a subtype')));
    expect(result.error.toString(), contains('wait-until'));
  });

  // REVIEW_VALIDATE-3: nested pipeline malformed cases
  test('build fails with validation message when exe-handle id is not a string',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe-handle:
        id: 123
        on-success:
          - exe: hello
        on-fail:
          - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('is not a subtype')));
    expect(result.error.toString(), contains('exe-handle'));
    expect(result.error.toString(), contains('id'));
  });

  // REVIEW_REVIEW_VALIDATE-1: pipeline.id validation
  test('build fails with validation message when pipeline.id is missing',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  workflow:
    - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('is not a subtype')));
    expect(result.error.toString(), contains('pipeline.id'));
  });

  test('build fails with validation message when pipeline.id is not a string',
      () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: 123
  workflow:
    - exe: hello
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('is not a subtype')));
    expect(result.error.toString(), contains('pipeline.id'));
  });

  test('build fails with validation message when workflow is empty', () async {
    const yaml = '''
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow: []
''';
    final result =
        await Application.instance.build(BuildType.file, yamlContent: yaml);
    expect(result.success, isFalse);
    expect(result.error.toString(), isNot(contains('Null check operator')));
    expect(result.error.toString(), contains('pipeline.workflow'));
  });

  test('CommandExe exe -f handles missing file without LateInitializationError',
      () async {
    final missing =
        '/tmp/oto_missing_${DateTime.now().microsecondsSinceEpoch}.yaml';
    expect(File(missing).existsSync(), isFalse);

    final command = CommandExe();
    final priorExitCode = exitCode;
    try {
      await command.execute(['-f', missing]);
    } finally {
      exitCode = priorExitCode;
    }
  });

  group('Application.validateYamlContent', () {
    const validYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

    test('validateYamlContent returns success contract for valid yaml', () {
      final result = Application.validateYamlContent(validYaml);
      expect(result.valid, isTrue);
      expect(result.phase, 'Validate YAML');
      expect(result.message, 'YAML is valid.');
      expect(result.exitCode, 0);
    });

    test('validateYamlContent returns build yaml failure contract', () {
      const invalidBuildYaml = '''
- command: Print
  id: hello
''';
      final result = Application.validateYamlContent(invalidBuildYaml);
      expect(result.valid, isFalse);
      expect(result.phase, 'Validate build yaml');
      expect(result.message, contains('Build YAML root must be a map'));
      expect(result.exitCode, 10);
    });

    test('validateYamlContent returns command list failure contract for duplicate id', () {
      const duplicateIdYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
  - command: Print
    id: hello
    param:
      message: hello again
pipeline:
  id: main
  workflow:
    - exe: hello
''';
      final result = Application.validateYamlContent(duplicateIdYaml);
      expect(result.valid, isFalse);
      expect(result.phase, 'Validate command list');
      expect(result.message, contains('Duplicate command id exists: "hello"'));
      expect(result.exitCode, 10);
    });

    test('validateYamlContent returns pipeline failure contract', () {
      const invalidPipelineYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: doesNotExist
''';
      final result = Application.validateYamlContent(invalidPipelineYaml);
      expect(result.valid, isFalse);
      expect(result.phase, 'Validate Pipeline');
      expect(result.message, contains('workflow[0]'));
      expect(result.exitCode, 10);
    });

    test('validateYamlContent exposes stable json shape', () {
      final result = Application.validateYamlContent(validYaml);
      final json = result.toJson();

      expect(json['schemaVersion'], 1);
      expect(json['type'], 'yamlValidation');
      expect(json['valid'], isTrue);
      expect(json['phase'], 'Validate YAML');
      expect(json['message'], 'YAML is valid.');
      expect(json['exitCode'], 0);
    });

    test('validateYamlContent returns failure contract for malformed scheduler section', () {
      const malformedSchedulerYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
scheduler: "not_a_map"
''';
      final result = Application.validateYamlContent(malformedSchedulerYaml);
      expect(result.valid, isFalse);
      expect(result.phase, 'Validate build yaml');
      expect(result.exitCode, 10);
      expect(result.message, isNotEmpty);
    });
  });

  group('BuildResult.toJson', () {
    test('exposes success execution envelope', () {
      const result = BuildResult.success();
      final json = result.toJson();

      expect(json['schemaVersion'], 1);
      expect(json['type'], 'executionResult');
      expect(json['success'], isTrue);
      expect(json['exitCode'], 0);
      expect(json['message'], 'Build completed successfully.');
      expect(json['error'], isNull);
      expect(json['stepEvents'], isEmpty);
    });

    test('exposes failure execution envelope', () {
      final error = Exception('Some critical error');
      final stack = StackTrace.current;
      final result = BuildResult.failure(error, stack, exitCode: 15);
      final json = result.toJson();

      expect(json['schemaVersion'], 1);
      expect(json['type'], 'executionResult');
      expect(json['success'], isFalse);
      expect(json['exitCode'], 15);
      expect(json['message'], contains('Some critical error'));
      expect(json['error'], isNotNull);
      expect(json['error']['type'], '_Exception');
      expect(json['error']['message'], contains('Some critical error'));
      expect(json['stepEvents'], isEmpty);
    });

    test('exposes populated step events', () {
      final events = [
        StepEvent(
          stepId: 0,
          workflowIndex: 0,
          stepType: 'exe',
          event: 'started',
          timestamp: '2026-05-22T05:00:00Z',
          commandId: 'hello',
          commandType: 'Print',
        ),
      ];
      final result = BuildResult.success(stepEvents: events);
      final json = result.toJson();

      expect(json['stepEvents'], hasLength(1));
      expect(json['stepEvents'][0]['stepId'], 0);
      expect(json['stepEvents'][0]['event'], 'started');
      expect(json['stepEvents'][0]['commandId'], 'hello');
      expect(json['stepEvents'][0]['commandType'], 'Print');
    });
  });

  group('BuildResult regression - stepEvents snapshot', () {
    test('first build result retains stepEvents after second build execution', () async {
      const validYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

      // 1. Run first build
      final result1 = await Application.instance.build(
        BuildType.file,
        yamlContent: validYaml,
        logEnable: false,
      );

      expect(result1.success, isTrue);
      final events1 = result1.toJson()['stepEvents'] as List;
      expect(events1, isNotEmpty);
      final originalEventsCount = events1.length;

      // 2. Run second build (which triggers context.resetStepEvents())
      final result2 = await Application.instance.build(
        BuildType.file,
        yamlContent: validYaml,
        logEnable: false,
      );

      expect(result2.success, isTrue);

      // 3. Assert first build result still retains its original stepEvents
      final events1AfterSecondBuild = result1.toJson()['stepEvents'] as List;
      expect(events1AfterSecondBuild, hasLength(originalEventsCount));
      expect(events1AfterSecondBuild[0]['event'], 'started');
      expect(events1AfterSecondBuild[0]['commandId'], 'hello');
    });
  });

  group('logEnable false', () {
    test('build with logEnable false suppresses failure stdout', () async {
      const invalidYaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: doesNotExist
''';

      final prints = <String>[];
      final result = await runZoned(() async {
        return await Application.instance.build(
          BuildType.file,
          yamlContent: invalidYaml,
          logEnable: false,
        );
      }, zoneSpecification: ZoneSpecification(
        print: (self, parent, zone, line) {
          prints.add(line);
        },
      ));

      expect(result.success, isFalse);
      expect(prints, isEmpty);
    });

    test('build with logEnable false suppresses command stdout logs', () async {
      const yaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';

      final prints = <String>[];
      final result = await runZoned(() async {
        return await Application.instance.build(
          BuildType.file,
          yamlContent: yaml,
          logEnable: false,
        );
      }, zoneSpecification: ZoneSpecification(
        print: (self, parent, zone, line) {
          prints.add(line);
        },
      ));

      expect(result.success, isTrue);
      expect(prints, isEmpty);
    });
  });

  test('scheduler build data runs without Jenkins env', () async {
    final logFile = File(
        '${Directory.systemTemp.path}/oto_scheduler_${DateTime.now().microsecondsSinceEpoch}.log');
    addTearDown(() {
      if (logFile.existsSync()) logFile.deleteSync();
    });
    const yaml = '''
scheduler:
  alias: local-test
  interval: 1000
  enableLog: true
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
    final build = DataBuild.fromJson(Application.getMapFromYamlA(yaml)!);

    final result = await Application.instance.build(
      BuildType.scheduler,
      buildData: build,
      logEnable: false,
      logPath: logFile.path,
    );

    expect(result.success, isTrue);
  });

  group('CommandExe JSON Mode', () {
    test('CommandExe shouldPrintExecuteLog suppresses execute log for json', () {
      final command = CommandExe();
      expect(command.shouldPrintExecuteLog(['--json']), isFalse);
      expect(command.shouldPrintExecuteLog(['-f', 'file.yaml']), isTrue);
    });

    test('CommandExe exe -f missing file --json returns execution result json', () async {
      final missing = '/tmp/oto_missing_${DateTime.now().microsecondsSinceEpoch}.yaml';
      expect(File(missing).existsSync(), isFalse);

      final prints = <String>[];
      final command = CommandExe(printString: (value) async {
        prints.add(value);
      });

      final priorExitCode = exitCode;
      try {
        await command.execute(['-f', missing, '--json']);
      } finally {
        exitCode = priorExitCode;
      }

      expect(prints, hasLength(1));
      final json = jsonDecode(prints[0]);
      expect(json['schemaVersion'], 1);
      expect(json['type'], 'executionResult');
      expect(json['success'], isFalse);
      expect(json['exitCode'], 10);
      expect(json['message'], contains('There are no files in path'));
      expect(json['stepEvents'], isEmpty);
    });

    test('actual bin exe --json output has parseable execution result json and no execute logs', () async {
      final tempFile = File('${Directory.systemTemp.path}/oto_temp_${DateTime.now().microsecondsSinceEpoch}.yaml');
      const yamlContent = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param:
      message: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
      tempFile.writeAsStringSync(yamlContent);

      try {
        final result = await Process.run('dart', [
          'run',
          'bin/main.dart',
          'exe',
          '-f',
          tempFile.path,
          '--json',
        ]);

        expect(result.exitCode, 0);

        final stdoutStr = result.stdout.toString().trim();

        expect(stdoutStr, isNotEmpty);

        final json = jsonDecode(stdoutStr);
        expect(json['schemaVersion'], 1);
        expect(json['type'], 'executionResult');
        expect(json['success'], isTrue);
        expect(json['exitCode'], 0);

        expect(json['stepEvents'], isA<List>());
        expect(json['stepEvents'].length, greaterThanOrEqualTo(2));
        expect(json['stepEvents'][0]['type'], 'stepEvent');
        expect(json['stepEvents'][0]['event'], 'started');
        expect(json['stepEvents'][0]['commandId'], 'hello');
        expect(json['stepEvents'].last['event'], 'completed');
        expect(json['stepEvents'].last['commandId'], 'hello');

        expect(stdoutStr, isNot(contains('Execute command:')));
        expect(stdoutStr, isNot(contains('hi')));
      } finally {
        if (tempFile.existsSync()) {
          tempFile.deleteSync();
        }
      }
    });
  });
}
