var nota = use('internal/nota');
var os = use('internal/os');
var blob = use('blob')

var EPSILON = 1e-12

function stone_if_needed(b) { if (!stone.p(b)) stone(b) }

function bytes_to_blob(bytes) {
  var b = blob()
  var i = 0
  var byte = null
  var bit = null
  for (i = 0; i < length(bytes); i++) {
    byte = bytes[i]
    for (bit = 7; bit >= 0; bit--) b.write_bit((byte >> bit) & 1)
  }
  stone(b)
  return b
}

function deepCompare(expected, actual, _path) {
  var diff = null
  var messages = null
  var expKeys = null
  var actKeys = null
  var i = 0
  var path = _path || ''
  if (expected == actual) return { passed: true, messages: [] };

  if (is_number(expected) && is_number(actual)) {
    if (isNaN(expected) && isNaN(actual))
      return { passed: true, messages: [] };

    diff = abs(expected - actual);
    if (diff <= EPSILON)
      return { passed: true, messages: [] };

    return {
      passed: false,
      messages: [
        `Value mismatch at ${path}: expected ${expected}, got ${actual}`,
        `Difference of ${diff} is larger than tolerance ${EPSILON}`
      ]
    };
  }

  if (is_blob(expected) && is_blob(actual)) {
    stone_if_needed(expected); stone_if_needed(actual)
    if (length(expected) != length(actual))
      return { passed: false, messages: [`blob length mismatch at ${path}: ${length(expected)} vs ${length(actual)}`] }
    for (i = 0; i < length(expected); i++) {
      if (expected.read_logical(i) != actual.read_logical(i))
        return { passed: false, messages: [`blob bit mismatch at ${path}[${i}]`] }
    }
    return { passed: true, messages: [] }
  }

  if (is_array(expected) && is_array(actual)) {
    if (length(expected) != length(actual))
      return {
        passed: false,
        messages: [`Array length mismatch at ${path}: expected ${length(expected)}, got ${length(actual)}`]
      };
    messages = [];
    arrfor(expected, function(val, i) {
      var result = deepCompare(val, actual[i], `${path}[${i}]`);
      if (!result.passed)
        messages = array(messages, result.messages)
    })
    return { passed: length(messages) == 0, messages: messages };
  }

  if (is_object(expected) && is_object(actual)) {
    expKeys = sort(array(expected))
    actKeys = sort(array(actual))
    if (JSON.stringify(expKeys) != JSON.stringify(actKeys))
      return {
        passed: false,
        messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`]
      };
    messages = [];
    arrfor(expKeys, function(key) {
      var result = deepCompare(expected[key], actual[key], `${path}.${key}`);
      if (!result.passed)
        messages = array(messages, result.messages)
    })
    return { passed: length(messages) == 0, messages: messages };
  }

  return {
    passed: false,
    messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`]
  };
}

function makeTest(test) {
  return function() {
    var encoded = test.replacer ? nota.encode(test.input, test.replacer) : nota.encode(test.input);
    var decoded = null
    var expected = null
    var key = null
    var compareResult = null
    if (!is_blob(encoded)) {
      disrupt
    }

    decoded = test.reviver ? nota.decode(encoded, test.reviver) : nota.decode(encoded);
    expected = test.expected || test.input;
    if (expected && (expected.private || expected.system)) {
      key = expected.private ? 'private' : 'system';
      expected = { [key]: expected[key] };
    }

    compareResult = deepCompare(expected, decoded);
    if (!compareResult.passed) {
      disrupt
    }
  };
}

var testarr = []
var i = 0
var t = null
for (i = 0; i < 500; i++) {
  push(testarr, 1)
}

var testCases = [
  { name: 'zero', input: 0 },
  { name: 'positive_2023', input: 2023 },
  { name: 'neg1', input: -1 },
  { name: 'positive_7', input: 7 },
  { name: 'neg7', input: -7 },
  { name: 'positive_1023', input: 1023 },
  { name: 'neg1023', input: -1023 },
  { name: 'null', input: null },
  { name: 'false', input: false },
  { name: 'true', input: true },
  { name: 'float_neg1_01', input: -1.01 },
  { name: 'float_98_6', input: 98.6 },
  { name: 'float_euler', input: -0.5772156649 },
  { name: 'float_precision', input: -1.00000000000001 },
  { name: 'float_large_neg', input: -10000000000000 },
  { name: 'empty_string', input: "" },
  { name: 'string_cat', input: "cat" },
  { name: 'string_unicode', input: "U+1F4A9 「うんち絵文字」 «💩»" },
  { name: 'buffer_ffaa', input: bytes_to_blob([0xFF, 0xAA]) },
  { name: 'buffer_f0e32080', input: bytes_to_blob([0b11110000, 0b11100011, 0b00100000, 0b10000000]) },
  { name: 'large_array', input: testarr },
  { name: 'empty_array', input: [] },
  { name: 'array_123', input: [1, 2, 3] },
  { name: 'array_mixed', input: [-1, 0, 1.5] },
  { name: 'empty_object', input: {} },
  { name: 'object_ab', input: { a: 1, b: 2 } },
  { name: 'object_nested', input: {
      num: 42,
      arr: [1, -1, 2.5],
      str: "test",
      obj: { x: true }
    } },
  { name: 'object_private', input: { private: { address: "test" } } },
  { name: 'object_system', input: { system: { msg: "hello" } } },
  { name: 'array_system_nested', input: [{ system: {msg: "hello" } }, {
      num: 42,
      arr: [1, -1, 2.5],
      str: "test",
      obj: { x: true }
    }] },
  { name: 'empty_buffer', input: blob() },
  { name: 'nested_empty_array', input: [[]] },
  { name: 'empty_key_value', input: { "": "" } },
  { name: 'small_float', input: 1e-10 },
  { name: 'replacer_multiply', input: { a: 1, b: 2 },
    replacer: (key, value) => is_number(value) ? value * 2 : value,
    expected: { a: 2, b: 4 } },
  { name: 'replacer_string_append', input: { x: "test", y: 5 },
    replacer: (key, value) => key == 'x' ? value + "!" : value,
    expected: { x: "test!", y: 5 } },
  { name: 'reviver_multiply', input: { a: 1, b: 2 },
    reviver: (key, value) => is_number(value) ? value * 3 : value,
    expected: { a: 3, b: 6 } },
  { name: 'reviver_increment', input: { x: "test", y: 10 },
    reviver: (key, value) => key == 'y' ? value + 1 : value,
    expected: { x: "test", y: 11 } }
];

var tests = {};
for (i = 0; i < length(testCases); i++) {
  t = testCases[i];
  tests[t.name] = makeTest(t);
}

return tests;
