import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/pipeline/pipeline.dart';
import 'package:oto/oto/pipeline/pipeline_exe.dart';
import 'package:test/test.dart';

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

  test('parses minimal build yaml into DataBuild', () {
    const yaml = '''
property:
  workspace: .
commands:
  - command: Print
    id: hello
    param: hi
pipeline:
  id: main
  workflow:
    - exe: hello
''';
    final map = Application.getMapFromYamlA(yaml);
    expect(map, isNotNull);

    final build = DataBuild.fromJson(map!);
    expect(build.property!['workspace'], '.');
    expect(build.commands, hasLength(1));
    expect(build.commands.first.id, 'hello');
    expect(build.commands.first.command, CommandType.Print);
    expect(build.pipeline!.id, 'main');
    expect(build.pipeline!.workflow, hasLength(1));
  });

  test('tag system preserves single tag object values', () {
    Application.instance.property['items'] = [1, 2, 3];
    Application.instance.property['config'] = {'a': 1};

    final listValue = Command.replaceTagValue('<!property.items>');
    expect(listValue, isA<List>());
    expect(listValue, equals([1, 2, 3]));

    final mapValue = Command.replaceTagValue('<!property.config>');
    expect(mapValue, isA<Map>());
    expect(mapValue, equals({'a': 1}));

    final embedded = Command.replaceTagValue('values=<!property.items>');
    expect(embedded, isA<String>());
    expect(embedded, contains('1'));
  });

  test('pipeline validation rejects missing command id', () {
    final result = Pipeline.pipelineInitialize([
      {'exe': 'doesNotExist'}
    ]);
    expect(result.enable, isFalse);
    expect(result.message, contains('doesNotExist'));
  });

  // ── Explicit context regression tests ──────────────────────────────────────
  // Each test puts a different value in the singleton vs. the explicit context
  // and asserts that flow-control uses the explicit context exclusively.

  ExecutionContext makeCtx(
      Map<String, dynamic> props, List<String> commandIds) {
    registerAllCommands();
    final ctx = ExecutionContext();
    ctx.property.addAll(props);
    for (final id in commandIds) {
      final cmd = DataCommand()
        ..command = CommandType.Print
        ..id = id
        ..param = {'message': id};
      ctx.dataCommandMap[id] = cmd;
    }
    return ctx;
  }

  test('PipelineIf evaluates condition from explicit context not singleton',
      () async {
    // Singleton: env = 'dev' → on-false; explicit: env = 'prod' → on-true
    Application.instance.property['env'] = 'dev';
    final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']);

    final result = Pipeline.pipelineInitialize([
      {
        'if': {
          'condition-string': '<!property.env> == prod',
          'on-true': [
            {'exe': 'onTrue'}
          ],
          'on-false': [
            {'exe': 'onFalse'}
          ],
        }
      }
    ], context: ctx);

    expect(result.enable, isTrue, reason: result.message);
    await result.pipeline!.execute();

    expect(ctx.commandStates['onTrue'], CommandState.complete);
    expect(ctx.commandStates['onFalse'], CommandState.ready);
  });

  test('PipelineForeach iterates list from explicit context not singleton',
      () async {
    // Singleton: items = [] (empty); explicit: items = [10, 20, 30]
    Application.instance.property['items'] = <dynamic>[];
    final ctx = makeCtx({
      'items': [10, 20, 30]
    }, [
      'print'
    ]);

    final result = Pipeline.pipelineInitialize([
      {
        'foreach': {
          'iterator': '<!property.items>',
          'setValue': '<@property.current>',
          'on-do': [
            {'exe': 'print'}
          ],
        }
      }
    ], context: ctx);

    expect(result.enable, isTrue, reason: result.message);
    await result.pipeline!.execute();

    // Loop ran 3 times using explicit context; last assigned value = 30
    expect(ctx.property['current'], 30);
    // Singleton must not have been modified
    expect(Application.instance.property.containsKey('current'), isFalse);
  });

  test('PipelineContain evaluates target from explicit context not singleton',
      () async {
    // Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list)
    Application.instance.property['target'] = 'xyz';
    final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']);

    final result = Pipeline.pipelineInitialize([
      {
        'contain': {
          'target': '<!property.target>',
          'list': ['foo', 'bar'],
          'on-true': [
            {'exe': 'onTrue'}
          ],
          'on-false': [
            {'exe': 'onFalse'}
          ],
        }
      }
    ], context: ctx);

    expect(result.enable, isTrue, reason: result.message);
    await result.pipeline!.execute();

    expect(ctx.commandStates['onTrue'], CommandState.complete);
    expect(ctx.commandStates['onFalse'], CommandState.ready);
  });

  // REVIEW_VALIDATE-2: malformed nested pipeline regression tests
  test('pipeline validation rejects if with no condition field', () {
    final result = Pipeline.pipelineInitialize([
      {
        'if': {'on-true': []}
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNotNull);
    expect(result.message, isNot(contains('is not a subtype')));
  });

  test('pipeline validation rejects foreach with non-string iterator', () {
    final result = Pipeline.pipelineInitialize([
      {
        'foreach': {
          'iterator': 123,
          'setValue': '<@property.item>',
          'on-do': [
            {'exe': 'someCmd'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNotNull);
    expect(result.message, contains('foreach'));
    expect(result.message, isNot(contains('is not a subtype')));
  });

  test('pipeline validation rejects switch with non-list cases', () {
    final result = Pipeline.pipelineInitialize([
      {
        'switch': {
          'condition-string': 'test',
          'cases': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNotNull);
    expect(result.message, contains('switch'));
    expect(result.message, isNot(contains('is not a subtype')));
  });

  test('pipeline validation rejects if condition without operator', () {
    final result = Pipeline.pipelineInitialize([
      {
        'if': {
          'condition-string': '<!property.env>',
          'on-true': [
            {'exe': 'hello'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, contains('supported operator'));
    expect(result.message, isNot(contains('LateInitializationError')));
  });

  test('pipeline validation rejects while condition without operator', () {
    final result = Pipeline.pipelineInitialize([
      {
        'while': {
          'condition-bool': '<!property.running>',
          'on-do': [
            {'exe': 'hello'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, contains('supported operator'));
    expect(result.message, isNot(contains('LateInitializationError')));
  });

  test('pipeline validation rejects wait-until condition without operator', () {
    final result = Pipeline.pipelineInitialize([
      {'wait-until-string': '<!state.buildStep>'}
    ]);
    expect(result.enable, isFalse);
    expect(result.message, contains('supported operator'));
    expect(result.message, isNot(contains('LateInitializationError')));
  });

  // REVIEW_REVIEW_VALIDATE-3: branch list field validation regression tests
  test('pipeline validation rejects exe-handle with non-list on-success', () {
    final result = Pipeline.pipelineInitialize([
      {
        'exe-handle': {
          'id': 'hello',
          'on-success': 'not-a-list',
          'on-fail': [
            {'exe': 'hello'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('exe-handle'));
    expect(result.message, contains('on-success'));
  });

  test('pipeline validation rejects exe-handle with non-list on-fail', () {
    final result = Pipeline.pipelineInitialize([
      {
        'exe-handle': {
          'id': 'hello',
          'on-success': [
            {'exe': 'hello'}
          ],
          'on-fail': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('exe-handle'));
    expect(result.message, contains('on-fail'));
  });

  test('pipeline validation rejects if with non-list on-true', () {
    final result = Pipeline.pipelineInitialize([
      {
        'if': {
          'condition-string': 'a == b',
          'on-true': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('if'));
    expect(result.message, contains('on-true'));
  });

  test('pipeline validation rejects if with non-list on-false', () {
    final result = Pipeline.pipelineInitialize([
      {
        'if': {
          'condition-string': 'a == b',
          'on-true': [
            {'exe': 'hello'}
          ],
          'on-false': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('if'));
    expect(result.message, contains('on-false'));
  });

  test('pipeline validation rejects while with non-list on-do', () {
    final result = Pipeline.pipelineInitialize([
      {
        'while': {
          'condition-bool': 'true',
          'on-do': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('while'));
    expect(result.message, contains('on-do'));
  });

  test('pipeline validation rejects foreach with non-list on-do', () {
    final result = Pipeline.pipelineInitialize([
      {
        'foreach': {
          'iterator': '<!property.items>',
          'setValue': '<@property.item>',
          'on-do': 'not-a-list',
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('foreach'));
    expect(result.message, contains('on-do'));
  });

  test('pipeline validation rejects switch with non-map case element', () {
    final result = Pipeline.pipelineInitialize([
      {
        'switch': {
          'condition-string': 'prod',
          'cases': ['not-a-map'],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('switch'));
    expect(result.message, contains('cases'));
  });

  test('pipeline validation rejects switch with missing tasks in case', () {
    final result = Pipeline.pipelineInitialize([
      {
        'switch': {
          'condition-string': 'prod',
          'cases': [
            {'value': 'prod'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('switch'));
    expect(result.message, contains('tasks'));
  });

  test('pipeline validation rejects switch with non-list tasks in case', () {
    final result = Pipeline.pipelineInitialize([
      {
        'switch': {
          'condition-string': 'prod',
          'cases': [
            {'value': 'prod', 'tasks': 'not-a-list'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('switch'));
    expect(result.message, contains('tasks'));
  });

  // REVIEW_REVIEW_VALIDATE-2: contain node guard regression tests
  test('pipeline validation rejects contain with non-map value', () {
    final result = Pipeline.pipelineInitialize([
      {'contain': 123}
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('contain'));
  });

  test('pipeline validation rejects contain with missing target', () {
    final result = Pipeline.pipelineInitialize([
      {
        'contain': {
          'list': ['a', 'b'],
          'on-true': [
            {'exe': 'x'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('contain'));
    expect(result.message, contains('target'));
  });

  test('pipeline validation rejects contain with non-string target', () {
    final result = Pipeline.pipelineInitialize([
      {
        'contain': {
          'target': 123,
          'list': ['a', 'b'],
          'on-true': [
            {'exe': 'x'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('contain'));
    expect(result.message, contains('target'));
  });

  test('pipeline validation rejects contain with missing list', () {
    final result = Pipeline.pipelineInitialize([
      {
        'contain': {
          'target': 'foo',
          'on-true': [
            {'exe': 'x'}
          ],
        }
      }
    ]);
    expect(result.enable, isFalse);
    expect(result.message, isNot(contains('is not a subtype')));
    expect(result.message, contains('contain'));
    expect(result.message, contains('list'));
  });

  test(
      'PipelineSwitch validate uses explicit context command map not singleton',
      () {
    // Singleton dataCommandMap is empty; only explicit context has the command.
    registerAllCommands();
    final ctx = ExecutionContext();
    final cmd = DataCommand()
      ..command = CommandType.Print
      ..id = 'deployProd'
      ..param = {'message': 'prod'};
    ctx.dataCommandMap['deployProd'] = cmd;

    final result = Pipeline.pipelineInitialize([
      {
        'switch': {
          'condition-string': 'prod',
          'cases': [
            {
              'value': 'prod',
              'tasks': [
                {'exe': 'deployProd'}
              ],
            }
          ],
        }
      }
    ], context: ctx);

    // Should succeed: deployProd exists in explicit context.
    // Would fail if singleton (empty) context were used.
    expect(result.enable, isTrue, reason: result.message);
  });

  test('pipeline execution records started and completed step events',
      () async {
    final ctx = makeCtx({}, ['hello']);
    final result = Pipeline.pipelineInitialize([
      {'exe': 'hello'}
    ], context: ctx);

    expect(result.enable, isTrue);
    final pipeline = result.pipeline!;
    await pipeline.execute();

    expect(ctx.stepEvents, hasLength(2));
    final started = ctx.stepEvents[0];
    final completed = ctx.stepEvents[1];

    expect(started.event, 'started');
    expect(started.stepId, 0);
    expect(started.workflowIndex, 0);
    expect(started.stepType, 'exe');
    expect(started.commandId, 'hello');
    expect(started.commandType, 'Print');

    expect(completed.event, 'completed');
    expect(completed.stepId, 0);
    expect(completed.workflowIndex, 0);
    expect(completed.stepType, 'exe');
    expect(completed.commandId, 'hello');
    expect(completed.commandType, 'Print');
  });

  test('pipeline execution records failed step event before rethrow', () async {
    final ctx = ExecutionContext();
    final throwing = ThrowingExecutor()
      ..context = ctx
      ..workflowKey = 'exe';

    final pipeline = Pipeline([throwing], ctx);

    await expectLater(
      pipeline.execute(),
      throwsA(isA<Exception>().having(
          (e) => e.toString(), 'message', contains('Test error message'))),
    );

    expect(ctx.stepEvents, hasLength(2));
    final started = ctx.stepEvents[0];
    final failed = ctx.stepEvents[1];

    expect(started.event, 'started');
    expect(started.stepId, 0);

    expect(failed.event, 'failed');
    expect(failed.stepId, 0);
    expect(failed.stepType, 'exe');
    expect(failed.error, isNotNull);
    expect(failed.error!['message'], contains('Test error message'));
  });
}

class ThrowingExecutor extends PipelineExecutor {
  @override
  PipelineValidateResult initialize(Map<String, dynamic> set) =>
      PipelineValidateResult();

  @override
  Future execute() => Future.error(Exception('Test error message'));
}
