// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:math';

import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

import 'problem_strings.dart';
import 'test_utils.dart';

/// Performs naive fuzzing on an initial YAML file based on an initial seed.
///
/// Starting with a template YAML, we randomly generate modifications and their
/// inputs (boolean, null, strings, or numbers) to modify the YAML and assert
/// that the change produced was expected.
void main() {
  const seed = 0;
  final generator = _Generator(seed: seed);

  const roundsOfTesting = 40;
  const modificationsPerRound = 1000;

  for (var i = 0; i < roundsOfTesting; i++) {
    test('testing with randomly generated modifications: test $i', () {
      final editor = YamlEditor('''
name: yaml_edit
description: A library for YAML manipulation with comment and whitespace preservation.
version: 0.0.1-dev

environment:
  sdk: ">=2.4.0 <3.0.0"

dependencies:
  meta: ^1.1.8
  quiver_hashcode: ^2.0.0

dev_dependencies:
  pedantic: ^1.9.0
  test: ^1.14.4
''');

      for (var j = 0; j < modificationsPerRound; j++) {
        /// Using [runZoned] to hide `package:yaml`'s warnings.
        /// Test failures and errors will still be shown.
        runZoned(() {
          expect(
              () => generator.performNextModification(editor), returnsNormally);
        },
            zoneSpecification: ZoneSpecification(
                print: (Zone self, ZoneDelegate parent, Zone zone,
                    String message) {}));
      }
    });
  }
}

/// Generates the random variables we need for fuzzing.
class _Generator {
  final Random r;

  /// 2^32
  static const int maxInt = 4294967296;

  /// Maximum depth of random YAML collection generated.
  final int maxDepth;

  // ignore: unused_element
  _Generator({int? seed, this.maxDepth = 5}) : r = Random(seed ?? 42);

  int nextInt([int max = maxInt]) => r.nextInt(max);

  double nextDouble() => r.nextDouble();

  bool nextBool() => r.nextBool();

  /// Generates a new string by individually generating characters and
  /// appending them to a buffer. Currently only generates strings from
  /// ascii 32 - 127.
  String nextString() {
    if (nextBool()) {
      return problemStrings[nextInt(problemStrings.length)];
    }

    final length = nextInt(100);
    final buffer = StringBuffer();

    for (var i = 0; i < length; i++) {
      final charCode = nextInt(95) + 32;
      buffer.writeCharCode(charCode);
    }

    return buffer.toString();
  }

  /// Generates a new scalar recognizable by YAML.
  Object? nextScalar() {
    final typeIndex = nextInt(5);

    switch (typeIndex) {
      case 0:
        return nextBool();
      case 1:
        return nextDouble();
      case 2:
        return nextInt();
      case 3:
        return null;
      default:
        return nextString();
    }
  }

  YamlScalar nextYamlScalar() {
    return wrapAsYamlNode(nextScalar(), scalarStyle: nextScalarStyle())
        as YamlScalar;
  }

  /// Generates the next [YamlList], with the current [depth].
  YamlList nextYamlList(int depth) {
    final length = nextInt(9);
    final list = [];

    for (var i = 0; i < length; i++) {
      list.add(nextYamlNode(depth + 1));
    }

    return wrapAsYamlNode(list, collectionStyle: nextCollectionStyle())
        as YamlList;
  }

  /// Generates the next [YamlList], with the current [depth].
  YamlMap nextYamlMap(int depth) {
    final length = nextInt(9);
    final nodes = {};

    for (var i = 0; i < length; i++) {
      nodes[nextYamlNode(depth + 1)] = nextYamlScalar();
    }

    return wrapAsYamlNode(nodes, collectionStyle: nextCollectionStyle())
        as YamlMap;
  }

  /// Returns a [YamlNode], with it being a [YamlScalar] 80% of the time, a
  /// [YamlList] 10% of the time, and a [YamlMap] 10% of the time.
  ///
  /// If [depth] is greater than [maxDepth], we instantly return a [YamlScalar]
  /// to prevent the parent from growing any further, to improve our speeds.
  YamlNode nextYamlNode([int depth = 0]) {
    if (depth >= maxDepth) {
      return nextYamlScalar();
    }

    final roll = nextInt(10);

    if (roll < 8) {
      return nextYamlScalar();
    } else if (roll == 8) {
      return nextYamlList(depth);
    } else {
      return nextYamlMap(depth);
    }
  }

  /// Performs a random modification
  void performNextModification(YamlEditor editor) {
    final path = findPath(editor);
    final node = editor.parseAt(path);
    final initialString = editor.toString();
    final args = [];
    var method = YamlModificationMethod.remove;

    try {
      if (node is YamlScalar) {
        editor.remove(path);
        return;
      }

      if (node is YamlList) {
        final methodIndex = nextInt(YamlModificationMethod.values.length);
        method = YamlModificationMethod.values[methodIndex];

        switch (method) {
          case YamlModificationMethod.remove:
            editor.remove(path);
            break;
          case YamlModificationMethod.update:
            if (node.isEmpty) break;
            final index = nextInt(node.length);
            args.add(nextYamlNode());
            path.add(index);
            editor.update(path, args[0]);
            break;
          case YamlModificationMethod.appendTo:
            args.add(nextYamlNode());
            editor.appendToList(path, args[0]);
            break;
          case YamlModificationMethod.prependTo:
            args.add(nextYamlNode());
            editor.prependToList(path, args[0]);
            break;
          case YamlModificationMethod.insert:
            args.add(nextInt(node.length + 1));
            args.add(nextYamlNode());
            editor.insertIntoList(path, args[0] as int, args[1]);
            break;
          case YamlModificationMethod.splice:
            args.add(nextInt(node.length + 1));
            args.add(nextInt(node.length + 1 - (args[0] as int)));
            args.add(nextYamlList(0));
            editor.spliceList(
                path, args[0] as int, args[1] as int, args[2] as List);
            break;
        }
        return;
      }

      if (node is YamlMap) {
        final replace = nextBool();
        method = YamlModificationMethod.update;

        if (replace && node.isNotEmpty) {
          final keyList = node.keys.toList();
          path.add(keyList[nextInt(keyList.length)]);
        } else {
          path.add(nextScalar());
        }
        final value = nextYamlNode();
        args.add(value);
        editor.update(path, value);
        return;
      }
    } catch (error, stacktrace) {
      print('''
Failed to call $method on:
$initialString
with the following arguments:
$args
and path:
$path

Error Details:
$error

$stacktrace
''');
      rethrow;
    }

    throw AssertionError('Got invalid node');
  }

  /// Obtains a random path by traversing [editor].
  ///
  /// At every node, we return the path to the node if the node has no children.
  /// Otherwise, we return at a 50% chance, or traverse to one random child.
  List<Object?> findPath(YamlEditor editor) {
    final path = <Object?>[];

    // 50% chance of stopping at the collection
    while (nextBool()) {
      final node = editor.parseAt(path);

      if (node is YamlList && node.isNotEmpty) {
        path.add(nextInt(node.length));
      } else if (node is YamlMap && node.isNotEmpty) {
        final keyList = node.keys.toList();
        path.add(keyList[nextInt(keyList.length)]);
      } else {
        break;
      }
    }

    return path;
  }

  ScalarStyle nextScalarStyle() {
    final seed = nextInt(6);

    switch (seed) {
      case 0:
        return ScalarStyle.DOUBLE_QUOTED;
      case 1:
        return ScalarStyle.FOLDED;
      case 2:
        return ScalarStyle.LITERAL;
      case 3:
        return ScalarStyle.PLAIN;
      case 4:
        return ScalarStyle.SINGLE_QUOTED;
      default:
        return ScalarStyle.ANY;
    }
  }

  CollectionStyle nextCollectionStyle() {
    final seed = nextInt(3);

    switch (seed) {
      case 0:
        return CollectionStyle.BLOCK;
      case 1:
        return CollectionStyle.FLOW;
      default:
        return CollectionStyle.ANY;
    }
  }
}
