test('valuesEquals', () => {
  fc.assert(
    fc.property(
      fc.oneof(fc.boolean(), fc.double(), fc.fullUnicodeString()),
      fc.oneof(fc.boolean(), fc.double(), fc.fullUnicodeString()),
      (v1, v2) => {
        expect(valuesEqual(v1, v2)).toBe(v1 === v2);
      },
    ),
  );

  fc.assert(
    fc.property(
      fc.constantFrom(null, undefined),
      fc.oneof(
        fc.constantFrom(null, undefined),
        fc.boolean(),
        fc.double(),
        fc.fullUnicodeString(),
      ),
      (v1, v2) => {
        expect(valuesEqual(v1, v2)).false;
        expect(valuesEqual(v2, v1)).false;
      },
    ),
  );
})


unknown

test('basics', () => {
  // nulls and undefined are false in all conditions except IS NULL and IS NOT NULL
  fc.assert(
    fc.property(
      fc.oneof(fc.constant(null), fc.constant(undefined)),
      fc.oneof(
        fc.constant('='),
        fc.constant('!='),
        fc.constant('<'),
        fc.constant('<='),
        fc.constant('>'),
        fc.constant('>='),
        fc.constant('LIKE'),
        fc.constant('NOT LIKE'),
        fc.constant('ILIKE'),
        fc.constant('NOT ILIKE'),
      ),
      // hexastring to avoid sending escape chars to like
      fc.oneof(fc.hexaString(), fc.double(), fc.boolean(), fc.constant(null)),
      (a, operator, b) => {
        const condition: SimpleCondition = {
          type: 'simple',
          left: {
            type: 'column',
            name: 'foo',
          },
          op: operator as SimpleOperator,
          right: {
            type: 'literal',
            value: b,
          },
        };
        const predicate = createPredicate(condition);
        expect(predicate({foo: a})).toBe(false);
      },
    ),
  );

  let condition: SimpleCondition = {
    type: 'simple',
    left: {
      type: 'column',
      name: 'foo',
    },
    op: 'IS',
    right: {
      type: 'literal',
      value: null,
    },
  };
  let predicate = createPredicate(condition);
  expect(predicate({foo: null})).toBe(true);
  expect(predicate({foo: 1})).toBe(false);
  expect(predicate({foo: 'null'})).toBe(false);
  expect(predicate({foo: true})).toBe(false);
  expect(predicate({foo: false})).toBe(false);

  condition = {
    type: 'simple',
    left: {
      type: 'column',
      name: 'foo',
    },
    op: 'IS NOT',
    right: {
      type: 'literal',
      value: null,
    },
  };
  predicate = createPredicate(condition);
  expect(predicate({foo: null})).toBe(false);
  expect(predicate({foo: 1})).toBe(true);
  expect(predicate({foo: 'null'})).toBe(true);
  expect(predicate({foo: true})).toBe(true);
  expect(predicate({foo: false})).toBe(true);

  // basic operators
  fc.assert(
    fc.property(
      fc.oneof(fc.boolean(), fc.double(), fc.string()),
      fc.oneof(
        fc.constant('='),
        fc.constant('!='),
        fc.constant('<'),
        fc.constant('<='),
        fc.constant('>'),
        fc.constant('>='),
      ),
      fc.oneof(fc.boolean(), fc.double(), fc.string()),
      (a, op, b) => {
        const condition: SimpleCondition = {
          type: 'simple',
          left: {
            type: 'column',
            name: 'foo',
          },
          op: op as SimpleOperator,
          right: {
            type: 'literal',
            value: b,
          },
        };
        const predicate = createPredicate(condition);
        const jsOp = {'=': '===', '!=': '!=='}[op] ?? op;
        expect(predicate({foo: a})).toBe(eval(`a ${jsOp} b`));
      },
    ),
  );
})


test('no clashes - single pk', () => {
  fc.assert(
    fc.property(
      fc.oneof(
        fc.tuple(fc.string(), fc.string()),
        fc.tuple(fc.double(), fc.double()),
        fc.tuple(fc.boolean(), fc.boolean()),
      ),
      ([a, b]) => {
        const keyA = toPrimaryKeyStringImpl('issue', ['id'], {id: a});
        const keyB = toPrimaryKeyStringImpl('issue', ['id'], {id: b});
        if (a === b) {
          expect(keyA).toBe(keyB);
        } else {
          expect(keyA).not.toBe(keyB);
        }
      },
    ),
  );
})


test('no clashes - multiple pk', () => {
  const primaryKey = ['id', 'name'] as const;
  fc.assert(
    fc.property(
      fc.tuple(
        fc.oneof(fc.string(), fc.double(), fc.boolean()),
        fc.oneof(fc.string(), fc.double(), fc.boolean()),
        fc.oneof(fc.string(), fc.double(), fc.boolean()),
        fc.oneof(fc.string(), fc.double(), fc.boolean()),
      ),
      ([a1, a2, b1, b2]) => {
        const keyA = toPrimaryKeyStringImpl('issue', primaryKey, {
          id: a1,
          name: a2,
        });
        const keyB = toPrimaryKeyStringImpl('issue', primaryKey, {
          id: b1,
          name: b2,
        });
        if (a1 === b1 && a2 === b2) {
          expect(keyA).toBe(keyB);
        } else {
          expect(keyA).not.toBe(keyB);
        }
      },
    ),
  );
})


test('random using fast check', () => {
  fc.assert(
    fc.property(
      fc.bigInt({min: 0n}),
      fc.integer({min: 2, max: 36}),
      (n, radix) => {
        const s = n.toString(radix);
        const actual = parseBigInt(s, radix);
        expect(actual).toBe(n);
      },
    ),
  );
})


test('random with fast-check', () => {
  fc.assert(
    fc.property(fc.float(), fc.float(), (a, b) => {
      const as = encodeFloat64AsString(a);
      const bs = encodeFloat64AsString(b);

      const a2 = decodeFloat64AsString(as);
      const b2 = decodeFloat64AsString(bs);

      expect(a2).toBe(a);
      expect(b2).toBe(b);

      if (Object.is(a, b)) {
        expect(as).toBe(bs);
      } else {
        expect(as).not.toBe(bs);
        if (!Number.isNaN(a) && !Number.isNaN(b)) {
          expect(as < bs).toBe(a < b);
        }
      }
    }),
  );
})


test('fuzz', () => {
    fc.assert(
      fc.property(
        fc.array(fc.array(fc.integer())),
        fc.boolean(),
        (arrays, noDupes) => {
          const sorted = arrays.map(a => a.slice().sort((l, r) => l - r));
          const result = mergeIterables(sorted, (l, r) => l - r, noDupes);
          const expected = sorted.flat().sort((l, r) => l - r);
          expect([...result]).toEqual(
            noDupes ? [...new Set(expected)] : expected,
          );
        },
      ),
    );
  })


UnitTest.test('KAssert.eqError: success (reflexivity)', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    KAssert.eqError('eq', i, Result.error(i));
    KAssert.eqError('eq', i, Result.error(i), tBoom());
    KAssert.eqError('eq', i, Result.error(i), tBoom(), tNumber);
  }));
})


UnitTest.test('KAssert.eqError: failure', () => {
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('should throw if numbers differ #1', () => {
      KAssert.eqError('eq', a, Result.error(b));
    });
    Assert.throws('should throw if numbers differ #2', () => {
      KAssert.eqError('eq', a, Result.error(b), tBoom());
    });
    Assert.throws('should throw if numbers differ #2', () => {
      KAssert.eqError('eq', a, Result.error(b), tBoom(), tNumber);
    });
  }));

  fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
    Assert.throws('should throw if value #1', () => {
      KAssert.eqError('eq', i, Result.value(s));
    });
    Assert.throws('should throw if value #2', () => {
      KAssert.eqError('eq', i, Result.value(s), tBoom());
    });
    Assert.throws('should throw if value #3', () => {
      KAssert.eqError('eq', i, Result.value(s), tBoom(), tBoom());
    });
  }));
})


UnitTest.test('KAssert.eqValue: success', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    KAssert.eqValue('eq', i, Result.value(i));
    KAssert.eqValue('eq', i, Result.value(i), tNumber);
    KAssert.eqValue('eq', i, Result.value(i), tNumber, tBoom());
  }));
})


UnitTest.test('KAssert.eqValue: failure', () => {
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('should throw if numbers differ #1', () => {
      KAssert.eqValue('eq', a, Result.value(b));
    });
    Assert.throws('should throw if numbers differ #2', () => {
      KAssert.eqValue('eq', a, Result.value(b), tNumber);
    });
    Assert.throws('should throw if numbers differ #2', () => {
      KAssert.eqValue('eq', a, Result.value(b), tNumber, tBoom());
    });
  }));

  fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
    Assert.throws('should throw if error #1', () => {
      KAssert.eqValue('eq', i, Result.error(s));
    });
    Assert.throws('should throw if error #2', () => {
      KAssert.eqValue('eq', i, Result.error(s), tBoom());
    });
    Assert.throws('should throw if error #3', () => {
      KAssert.eqValue('eq', i, Result.error(s), tBoom(), tBoom());
    });
  }));
})


UnitTest.test('KAssert.eqResult: success', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    KAssert.eqResult('eq', Result.value(i), Result.value(i));
    KAssert.eqResult('eq', Result.value(i), Result.value(i), tNumber);
    KAssert.eqResult('eq', Result.value(i), Result.value(i), tNumber, tBoom());
    KAssert.eqResult('eq', Result.error(i), Result.error(i));
    KAssert.eqResult('eq', Result.error(i), Result.error(i), tBoom());
    KAssert.eqResult('eq', Result.error(i), Result.error(i), tBoom(), tNumber);
  }));
})


UnitTest.test('KAssert.eqResult: fail', () => {
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('value(a) != (value(!a)) #1', () => {
      KAssert.eqResult('eq', Result.value(a), Result.value(b));
    });
    Assert.throws('value(a) != (value(!a)) #2', () => {
      KAssert.eqResult('eq', Result.value(a), Result.value(b), tNumber);
    });
    Assert.throws('value(a) != (value(!a)) #2', () => {
      KAssert.eqResult('eq', Result.value(a), Result.value(b), tNumber, tBoom());
    });
  }));

  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('error(a) != (error(!a)) #1', () => {
      KAssert.eqResult('eq', Result.error(a), Result.error(b));
    });
    Assert.throws('error(a) != (error(!a)) #2', () => {
      KAssert.eqResult('eq', Result.error(a), Result.error(b), tBoom());
    });
    Assert.throws('result(a) != (result(!a)) #2', () => {
      KAssert.eqResult('eq', Result.error(a), Result.error(b), tBoom(), tNumber);
    });
  }));

  fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
    Assert.throws('value != error #1', () => {
      KAssert.eqResult('eq', Result.value(i), Result.error(s));
    });
    Assert.throws('value != error #2', () => {
      KAssert.eqResult('eq', Result.value(i), Result.error(s), tBoom());
    });
    Assert.throws('value != error #3', () => {
      KAssert.eqResult('eq', Result.value(i), Result.error(s), tBoom());
    });

    Assert.throws('error != value #1', () => {
      KAssert.eqResult('eq', Result.error(i), Result.value(s));
    });
    Assert.throws('error != value #2', () => {
      KAssert.eqResult('eq', Result.error(i), Result.value(s), tBoom());
    });
    Assert.throws('error != value #3', () => {
      KAssert.eqResult('eq', Result.error(i), Result.value(s), tBoom(), tBoom());
    });
  }));
})


UnitTest.test('KAssert.eqSome: success (reflexivity)', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    KAssert.eqSome('eq', i, Optional.some(i));
    KAssert.eqSome('eq', i, Optional.some(i), tNumber);
  }));
})


UnitTest.test('KAssert.eqSome: failure', () => {
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('some(i) != some(!i) #1', () => {
      KAssert.eqSome('eq', a, Optional.some(b));
    });
  }));
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('some(i) != some(!i) #2', () => {
      KAssert.eqSome('eq', a, Optional.some(b), tNumber);
    });
  }));
})


UnitTest.test('KAssert.eqNone: failure', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    Assert.throws('some(i) != none', () => {
      KAssert.eqNone('eq', Optional.some(i));
    });
  }));
})


UnitTest.test('KAssert.eqOption: success (reflexivity)', () => {
  fc.assert(fc.property(fc.integer(), (i) => {
    KAssert.eqOptional('eq', Optional.some(i), Optional.some(i));
    KAssert.eqOptional('eq', Optional.some(i), Optional.some(i), tNumber);
  }));
  KAssert.eqOptional('eq', Optional.none(), Optional.none());
})


UnitTest.test('KAssert.eqOption: failure', () => {
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('some(i) != some(!i) #1', () => {
      KAssert.eqOptional('eq', Optional.some(a), Optional.some(b));
    });
  }));
  fc.assert(fc.property(twoDifferentNumbers, ([ a, b ]) => {
    Assert.throws('some(i) != some(!i) #2', () => {
      KAssert.eqOptional('eq', Optional.some(a), Optional.some(b), tNumber);
    });
  }));
  fc.assert(fc.property(fc.integer(), (i) => {
    Assert.throws('none != some(i) #1', () => {
      KAssert.eqOptional('eq', Optional.none(), Optional.some(i));
    });
    Assert.throws('none != some(i) #2', () => {
      KAssert.eqOptional('eq', Optional.none(), Optional.some(i), tBoom());
    });
  }));
  fc.assert(fc.property(fc.integer(), (i) => {
    Assert.throws('some(i) != none #1', () => {
      KAssert.eqOptional('eq', Optional.some(i), Optional.none());
    });
    Assert.throws('some(i) != none #2', () => {
      KAssert.eqOptional('eq', Optional.some(i), Optional.none(), tBoom());
    });
  }));
})


it('TINY-6891: Should be able to convert to and from numbers/list numbering', () => {
    const arbitrary = Arr.map([
      fc.mapToConstant({ num: 26, build: (v) => String.fromCharCode(65 + v) }),
      fc.mapToConstant({ num: 26, build: (v) => String.fromCharCode(97 + v) }),
      fc.mapToConstant({ num: 10, build: (v) => v.toString() })
    ], (c) => fc.stringOf(c, 1, 5));

    fc.assert(fc.property(
      fc.oneof(...arbitrary),
      (start) => {
        assert.equal(
          start,
          parseStartValue(start).map(parseDetail).getOrDie(),
          'Should be initial start value'
        );
      }
    ), { endOnFailure: true });
  })


it('no initial data', () => {
      const dialogChange = DialogChanges.init({ title: '', text: '' } as LinkDialogData, { } as LinkDialogCatalog);

      fc.assert(fc.property(fc.webUrl(), fc.asciiString(), fc.asciiString(), (url, title, text) => {
        const data = Fun.constant({ url: {
          value: url,
          meta: { title, text }
        }} as LinkDialogData);
        const dataNoMeta = Fun.constant({ url: {
          value: url,
          meta: { }
        }} as LinkDialogData);

        Assert.eq('on url change should include url title and text',
          Optional.some<Partial<LinkDialogData>>({ title, text }),
          dialogChange.onChange(data, { name: 'url' }),
          tOptional()
        );

        Assert.eq('on url change should fallback to url for text',
          Optional.some<Partial<LinkDialogData>>({ title: '', text: url }),
          dialogChange.onChange(dataNoMeta, { name: 'url' }),
          tOptional()
        );
      }));
    })


it('with original data', () => {
      const dialogChange = DialogChanges.init({ title: 'orig title', text: 'orig text' } as LinkDialogData, { } as LinkDialogCatalog);
      const dialogChangeNoTitle = DialogChanges.init({ title: '', text: 'orig text' } as LinkDialogData, { } as LinkDialogCatalog);
      const dialogChangeNoText = DialogChanges.init({ title: 'orig title', text: '' } as LinkDialogData, { } as LinkDialogCatalog);

      fc.assert(fc.property(fc.webUrl(), fc.asciiString(), fc.asciiString(), (url, title, text) => {
        const data = Fun.constant({ url: {
          value: url,
          meta: { title, text }
        }} as LinkDialogData);

        Assert.eq('on url change should not try to change title and text',
          Optional.none(),
          dialogChange.onChange(data, { name: 'url' }),
          tOptional()
        );

        Assert.eq('No Title - on url change should only try to change title',
          Optional.some<Partial<LinkDialogData>>({ title }),
          dialogChangeNoTitle.onChange(data, { name: 'url' }),
          tOptional()
        );

        Assert.eq('No Text - on url change should only try to change text',
          Optional.some<Partial<LinkDialogData>>({ text }),
          dialogChangeNoText.onChange(data, { name: 'url' }),
          tOptional()
        );
      }));
    })


it('TINY-4773: multiple @ characters', () => {
    const editor = hook.editor();
    fc.assert(fc.property(fc.hexaString(0, 30), fc.hexaString(0, 30), fc.hexaString(0, 30), (s1, s2, s3) => {
      assertNoLink(editor, `${s1}@@${s2}@.@${s3}`, `${s1}@@${s2}@.@${s3}`);
    }));
  })


it('TINY-4773: ending in @ character', () => {
    const editor = hook.editor();
    fc.assert(fc.property(fc.hexaString(0, 100), (s1) => {
      assertNoLink(editor, `${s1}@`, `${s1}@`);
    }));
  })


UnitTest.test('ShadowDom - SelectorFind.descendant', () => {
  if (SugarShadowDom.isSupported()) {
    fc.assert(fc.property(htmlBlockTagName(), htmlInlineTagName(), fc.hexaString(), (block, inline, text) => {
      withShadowElement((ss) => {
        const id = 'theid';
        const inner = SugarElement.fromHtml(`<${block}><p>hello<${inline} id="${id}">${text}</${inline}></p></${block}>`);
        Insert.append(ss, inner);

        const frog: SugarElement<Element> = SelectorFind.descendant(ss, `#${id}`).getOrDie('Element not found');
        Assert.eq('textcontent', text, frog.dom.textContent);
      });
    }));
  }
})


UnitTest.test('All valid floats are valid', () => {
  fc.assert(fc.property(fc.oneof(
    // small to medium floats
    fc.float(),
    // big floats
    fc.tuple(fc.float(), fc.integer(-20, 20)).map(([ mantissa, exponent ]) => mantissa * Math.pow(10, exponent))
  ), (num) => {
    const parsed = Dimension.parse(num.toString(), [ 'empty' ]).getOrDie();
    Assert.eq('Number is unchanged by stringifying and parsing', num, parsed.value);
    Assert.eq('Unit is empty', '', parsed.unit);
    return true;
  }));
})


UnitTest.test('All valid integers are valid', () => {
  fc.assert(fc.property(fc.integer(), (num) => {
    const parsed = Dimension.parse(num.toString(), [ 'empty' ]).getOrDie();
    Assert.eq('Number is unchanged by stringifying and parsing', num, parsed.value);
    Assert.eq('Unit is empty', '', parsed.unit);
    return true;
  }));
})


it('adt.exact.match should be same as fold', () => {
    fc.assert(fc.property(arbExact, (subject) => {
      const matched = subject.match({
        nothing: Fun.die('should not be nothing'),
        unknown: Fun.die('should not be unknown'),
        exact: record
      });

      const folded = subject.fold(Fun.die('should not be nothing'), Fun.die('should not be unknown'), record);
      assert.deepEqual(folded, matched);
    }));
  })

Summary: `adt.exact.match` produces the same result as `fold` when both are supplied with the same `exact` function.


it('leftTrim(whitespace + s) === leftTrim(s)', () => {
    fc.assert(fc.property(
      fc.string(),
      (s) => {
        assert.equal(Strings.lTrim(s), Strings.lTrim(' ' + s));
      }
    ));
  })

Summary: `leftTrim` applied to a string with added leading whitespace should yield the same result as applying `leftTrim` to the original string.


it('StringMatch.matches(StringMatch.starts(s1), s1 + s) === true', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (s, s1) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.starts(s1, StringMatch.caseInsensitive),
          s1 + s
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.contains(s1), s1 + s) === true', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (s, s1) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.contains(s1, StringMatch.caseInsensitive),
          s1 + s
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.contains(s1), s + s1) === true', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (s, s1) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.contains(s1, StringMatch.caseInsensitive),
          s + s1
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.contains(s1), s) === s.indexOf(s1)', () => {
    fc.assert(fc.property(
      fc.asciiString(),
      fc.asciiString(1, 40),
      (s, s1) => {
        assert.equal(StringMatch.matches(
          StringMatch.contains(s1, StringMatch.caseInsensitive),
          s
        ), s.toLowerCase().indexOf(s1.toLowerCase()) > -1);
      }
    ));
  })


it('StringMatch.matches(StringMatch.contains(s), s) === true', () => {
    fc.assert(fc.property(
      fc.string(),
      (s) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.contains(s, StringMatch.caseInsensitive),
          s
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.exact(s), s + s1) === false', () => {
    fc.assert(fc.property(
      fc.string(1, 40),
      fc.string(1, 40),
      (s, s1) => {
        assert.isFalse(StringMatch.matches(
          StringMatch.exact(s, StringMatch.caseInsensitive),
          s + s1
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.exact(s), s1 + s) === false', () => {
    fc.assert(fc.property(
      fc.string(1, 40),
      fc.string(1, 40),
      (s, s1) => {
        assert.isFalse(StringMatch.matches(
          StringMatch.exact(s, StringMatch.caseInsensitive),
          s1 + s
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.exact(s), s) === true', () => {
    fc.assert(fc.property(
      fc.string(1, 40),
      (s) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.exact(s, StringMatch.caseInsensitive),
          s
        ));
      }
    ));
  })


it('StringMatch.matches(StringMatch.exact(s), s) === false when different case and case-insensitive', () => {
    fc.assert(fc.property(
      fc.asciiString(1, 40),
      (s) => s.toUpperCase() === s.toLowerCase() || StringMatch.matches(
        StringMatch.exact(s.toLowerCase(), StringMatch.caseInsensitive),
        s.toUpperCase()
      )
    ));
  })


it('StringMatch.matches(StringMatch.exact(s), s) === false when different case and case-sensitive', () => {
    fc.assert(fc.property(
      fc.asciiString(1, 40),
      (s) => s.toUpperCase() === s.toLowerCase() || !StringMatch.matches(
        StringMatch.exact(s.toLowerCase(), StringMatch.caseSensitive),
        s.toUpperCase()
      )
    ));
  })


it('StringMatch.matches(StringMatch.all(s1), *) === true', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (s) => {
        assert.isTrue(StringMatch.matches(
          StringMatch.all(),
          s
        ));
      }
    ));
  })


it('positive range', () => {
    fc.assert(fc.property(fc.char(), fc.integer(1, 100), (c, count) => {
      const actual = Strings.repeat(c, count);
      assert.equal(actual.length, count);
      assert.equal(actual.charAt(0), c);
      assert.equal(actual.charAt(actual.length - 1), c);
    }));

    fc.assert(fc.property(fc.asciiString(5), fc.integer(1, 100), (s, count) => {
      const actual = Strings.repeat(s, count);
      assert.equal(actual.length, count * s.length);
      assert.equal(actual.indexOf(s), 0);
      assert.equal(actual.lastIndexOf(s), actual.length - s.length);
    }));
  })


it('negative range', () => {
    fc.assert(fc.property(fc.char(), fc.integer(-100, 0), (c, count) => {
      const actual = Strings.repeat(c, count);
      assert.equal(actual.length, 0);
      assert.equal(actual, '');
    }));
  })


it('removeTrailing property', () => {
    fc.assert(fc.property(
      fc.asciiString(),
      fc.asciiString(),
      (prefix, suffix) => {
        assert.equal(Strings.removeTrailing(prefix + suffix, suffix), prefix);
      }
    ));
  })


it('removeLeading removes prefix', () => {
    fc.assert(fc.property(
      fc.asciiString(),
      fc.asciiString(),
      (prefix, suffix) => {
        assert.equal(Strings.removeLeading(prefix + suffix, prefix), suffix);
      }
    ));
  })


it('ensureTrailing is identity if string already ends with suffix', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (prefix, suffix) => {
        const s = prefix + suffix;
        assert.equal(Strings.ensureTrailing(s, suffix), s);
      }));
  })


it('ensureTrailing endsWith', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(),
      (s, suffix) => Strings.endsWith(Strings.ensureTrailing(s, suffix), suffix)
    ));
  })


it('startsWith a prefix', () => {
    fc.assert(fc.property(fc.string(), fc.string(),
      (prefix, suffix) => Strings.startsWith(prefix + suffix, prefix)
    ));
  })


it('A string added to a string (at the end) must have endsWith as true', () => {
    fc.assert(fc.property(
      fc.asciiString(),
      fc.asciiString(),
      (str, contents) => Strings.endsWith(str + contents, contents)
    ));
  })


it('A string with length 1 or larger should never be empty', () => {
    fc.assert(fc.property(fc.string(1, 40), (str) => {
      assert.isFalse(Strings.isEmpty(str));
      assert.isTrue(Strings.isNotEmpty(str));
    }));
  })


it('A string added to a string (at the end) must be contained within it', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(1, 40),
      (str, contents) => {
        const r = str + contents;
        assert.isTrue(Strings.contains(r, contents));
      }
    ));
  })


it('A string added to a string (at the start) must be contained within it', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.string(1, 40),
      (str, contents) => {
        const r = contents = str;
        assert.isTrue(Strings.contains(r, contents));
      }
    ));
  })


it('An empty string should contain nothing', () => {
    fc.assert(fc.property(
      fc.string(1, 40),
      (value) => !Strings.contains('', value)
    ));
  })


it('tail of the string is unchanged', () => {
    fc.assert(fc.property(fc.ascii(), fc.asciiString(30), (h, t) => {
      assert.equal(Strings.capitalize(h + t).substring(1), t);
    }));
  })


it('head is uppercase', () => {
    fc.assert(fc.property(fc.ascii(), fc.asciiString(30), (h, t) => {
      const actualH = Strings.capitalize(h + t).charAt(0);
      assert.equal(actualH, h.toUpperCase());
    }));
  })


it('Optionals.cat of Arr.map(xs, Optional.some) should be xs', () => {
    fc.assert(fc.property(
      fc.array(fc.json()),
      (arr) => {
        const options = Arr.map(arr, Optional.some);
        const output = Optionals.cat(options);
        assert.deepEqual(output, arr);
      }
    ));
  })

Summary: `Optionals.cat` applied to an array of `Optional.some` values should reconstruct the original array.


it('Single some value', () => {
    fc.assert(fc.property(fc.integer(), (n) => {
      assertOptional(Optionals.sequence([ Optional.some(n) ]), Optional.some([ n ]));
    }));
  })

Summary: `Optionals.sequence` with a single `Optional.some` value should return `Optional.some` containing an array with that value.


it('someIf(false) is none', () => {
    fc.assert(fc.property(fc.integer(), (n) => {
      assertNone(Optionals.someIf<number>(false, n));
    }));
  })


it('someIf(true) is some', () => {
    fc.assert(fc.property(fc.integer(), (n) => {
      assertSome(Optionals.someIf<number>(true, n), n);
    }));
  })


it('None then some', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (n) => {
      const someNumbers = Arr.map(n, (x) => Optional.some(x));
      assertNone(Optionals.sequence<number>([ Optional.none<number>(), ...someNumbers ]));
    }));
  })


it('all some', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (n) =>
      assertOptional(Optionals.sequence<number>(Arr.map(n, (x) => Optional.some(x))), Optionals.traverse<number, number>(n, (x) => Optional.some(x)))
    ));
  })


it('flattens some(some(x)) to some(x)', () => {
    fc.assert(fc.property(fc.integer(), (n) => {
      assertOptional(Optionals.flatten(Optional.some(Optional.some<number>(n))), Optional.some(n));
    }));
  })


it('Optional.none() !== Optional.some(x)', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      assert.isFalse(Optionals.equals(Optional.none(), Optional.some(i)));
    }));
  })


it('Optional.some(x) !== Optional.none()', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      assert.isFalse((Optionals.equals(Optional.some(i), Optional.none())));
    }));
  })


it('Optional.some(x) === Optional.some(x)', () => {
    fc.assert(fc.property(fc.integer(), (i) => assert.isTrue(Optionals.equals(Optional.some(i), Optional.some(i)))));
  })


it('Optional.some(x) === Optional.some(x) (same ref)', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      const ob = Optional.some(i);
      assert.isTrue(Optionals.equals(ob, ob));
    }));
  })


it('Optional.some(x) !== Optional.some(x + y) where y is not identity', () => {
    fc.assert(fc.property(fc.string(), fc.string(1, 40), (a, b) => {
      assert.isFalse(Optionals.equals(Optional.some(a), Optional.some(a + b)));
    }));
  })


it('some !== none, for any predicate', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt1) => {
      assert.isFalse(Optionals.equals(opt1, Optional.none(), boom));
    }));
  })


it('none !== some, for any predicate', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt1) => {
      assert.isFalse(Optionals.equals(Optional.none(), opt1, boom));
    }));
  })


it('Checking Optionals.equals(some(x), some(y), _, _ -> false) === false', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), arbOptionSome(fc.integer()), (opt1, opt2) => {
      assert.isFalse(Optionals.equals(opt1, opt2, Fun.never));
    }));
  })


it('Checking Optionals.equals(some(x), some(y), _, _ -> true) === true', () => {
    fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => {
      const opt1 = Optional.some(a);
      const opt2 = Optional.some(b);
      assert.isTrue(Optionals.equals(opt1, opt2, Fun.always));
    }));
  })


it('Checking Optionals.equals(some(x), some(y), f) iff. f(x, y)', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), arbOptionSome(fc.integer()), fc.func(fc.boolean()), (a, b, f) => {
      const opt1 = Optional.some(a);
      const opt2 = Optional.some(b);
      return f(a, b) === Optionals.equals(opt1, opt2, f);
    }));
  })


it('Checking some(x).fold(die, id) === x', () => {
    fc.assert(fc.property(fc.integer(), (json) => {
      const opt = Optional.some(json);
      const actual = opt.fold(Fun.die('Should not be none!'), Fun.identity);
      assert.deepEqual(actual, json);
    }));
  })


it('Checking some(x).is(x) === true', () => {
    fc.assert(fc.property(fc.integer(), (json) => {
      const opt = Optional.some(json);
      assert.isTrue(Optionals.is(opt, json));
    }));
  })


it('Checking some(x).isSome === true', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => opt.isSome()));
  })


it('Checking some(x).isNone === false', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => !opt.isNone()));
  })


it('Checking some(x).getOr(v) === x', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), arbOptionSome(fc.integer()), (a, b) => {
      assert.equal(Optional.some(a).getOr(b), a);
    }));
  })


it('Checking some(x).getOrThunk(_ -> v) === x', () => {
    fc.assert(fc.property(fc.integer(), fc.func(fc.integer()), (a, thunk) => {
      assert.equal(Optional.some(a).getOrThunk(thunk), a);
    }));
  })


it('Checking some.getOrDie() never throws', () => {
    fc.assert(fc.property(fc.integer(), fc.string(1, 40), (i, s) => {
      const opt = Optional.some(i);
      opt.getOrDie(s);
    }));
  })


it('Checking some(x).or(oSomeValue) === some(x)', () => {
    fc.assert(fc.property(fc.integer(), arbOptionSome(fc.integer()), (json, other) => {
      const output = Optional.some(json).or(other);
      assert.isTrue(Optionals.is(output, json));
    }));
  })


it('Checking some(x).orThunk(_ -> oSomeValue) === some(x)', () => {
    fc.assert(fc.property(fc.integer(), arbOptionSome(fc.integer()), (i, other) => {
      const output = Optional.some(i).orThunk(() => other);
      assert.isTrue(Optionals.is(output, i));
    }));
  })


it('Checking some(x).map(f) === some(f(x))', () => {
    fc.assert(fc.property(fc.integer(), fc.func(fc.integer()), (a, f) => {
      const opt = Optional.some(a);
      const actual = opt.map(f);
      assert.equal(actual.getOrDie(), f(a));
    }));
  })


it('Checking some(x).each(f) === undefined and f gets x', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => {
      let hack: number | null = null;
      const actual = opt.each((x) => {
        hack = x;
      });
      assert.isUndefined(actual);
      assert.equal(opt.getOrDie(), hack);
    }));
  })


it('Given f :: s -> some(b), checking some(x).bind(f) === some(b)', () => {
    fc.assert(fc.property(fc.integer(), fc.func(arbOptionSome(fc.integer())), (i, f) => {
      const actual = Optional.some(i).bind(f);
      assert.deepEqual(actual, f(i));
    }));
  })


it('Given f :: s -> none, checking some(x).bind(f) === none', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), fc.func(arbOptionNone()), (opt, f) => {
      const actual = opt.bind(f);
      assertNone(actual);
    }));
  })


it('Checking some(x).exists(_ -> false) === false', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => !opt.exists(Fun.never)));
  })


it('Checking some(x).exists(_ -> true) === true', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => opt.exists(Fun.always)));
  })


it('Checking some(x).exists(f) iff. f(x)', () => {
    fc.assert(fc.property(fc.integer(), fc.func(fc.boolean()), (i, f) => {
      const opt = Optional.some(i);
      if (f(i)) {
        assert.isTrue(opt.exists(f));
      } else {
        assert.isFalse(opt.exists(f));
      }
    }));
  })


it('Checking some(x).forall(_ -> false) === false', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => !opt.forall(Fun.never)));
  })


it('Checking some(x).forall(_ -> true) === true', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => opt.forall(Fun.always)));
  })


it('Checking some(x).forall(f) iff. f(x)', () => {
    fc.assert(fc.property(fc.integer(), fc.func(fc.boolean()), (i, f) => {
      const opt = Optional.some(i);
      if (f(i)) {
        assert.isTrue(opt.forall(f));
      } else {
        assert.isFalse(opt.forall(f));
      }
    }));
  })


it('Checking some(x).toArray equals [ x ]', () => {
    fc.assert(fc.property(fc.integer(), (json) => {
      assert.deepEqual(Optional.some(json).toArray(), [ json ]);
    }));
  })


it('Checking some(x).toString equals "some(x)"', () => {
    fc.assert(fc.property(fc.integer(), (json) => {
      assert.equal(Optional.some(json).toString(), 'some(' + json + ')');
    }));
  })


it('Checking none.fold(_ -> x, die) === x', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      const actual = Optional.none<string>().fold(Fun.constant(i), Fun.die('Should not be called'));
      assert.equal(actual, i);
    }));
  })


it('Checking none.is === false', () => {
    fc.assert(fc.property(fc.integer(), (v) => {
      assert.isFalse(Optionals.is(Optional.none(), v));
    }));
  })


it('Checking none.getOr(v) === v', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      assert.equal(Optional.none<number>().getOr(i), i);
    }));
  })


it('Checking none.getOrThunk(_ -> v) === v', () => {
    fc.assert(fc.property(fc.func(fc.integer()), (thunk) => {
      assert.equal(Optional.none<number>().getOrThunk(thunk), thunk());
    }));
  })


it('Checking none.getOrDie() always throws', () => {
  // Require non empty string of msg falsiness gets in the way.
    fc.assert(fc.property(fc.string(1, 40), (s) => {
      assert.throws(() => {
        Optional.none().getOrDie(s);
      });
    }));
  })


it('Checking none.or(oSomeValue) === oSomeValue', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      const output = Optional.none().or(Optional.some(i));
      assert.isTrue(Optionals.is(output, i));
    }));
  })


it('Checking none.orThunk(_ -> v) === v', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      const output = Optional.none().orThunk(() => Optional.some(i));
      assert.isTrue(Optionals.is(output, i));
    }));
  })


it('none is not some', () => {
    assert.isFalse(Optional.none().isSome());
    fc.assert(fc.property(fc.anything(), (x) => {
      assert.isTrue(Optional.some(x).isSome());
    }));
  })


it('Optional.isNone', () => {
    assert.isTrue(Optional.none().isNone());
    fc.assert(fc.property(fc.anything(), (x) => {
      assert.isFalse(Optional.some(x).isNone());
    }));
  })


it('Optional.getOr', () => {
    fc.assert(fc.property(fc.integer(), (x) => {
      assert.equal(Optional.none().getOr(x), x);
      assert.equal(Optional.none().getOrThunk(() => x), x);
    }));
    fc.assert(fc.property(fc.integer(), fc.integer(), (x, y) => {
      assert.equal(Optional.some(x).getOr(y), x);
      assert.equal(Optional.some(x).getOrThunk(Fun.die('boom')), x);
    }));
  })


it('Checking some(x).filter(_ -> false) === none', () => {
    fc.assert(fc.property(arbOptionSome(fc.integer()), (opt) => {
      assertNone(opt.filter(Fun.never));
    }));
  })


it('Checking some(x).filter(_ -> true) === some(x)', () => {
    fc.assert(fc.property(fc.integer(), (x) => {
      assertSome(some(x).filter(Fun.always), x);
    }));
  })


it('Checking some(x).filter(f) === some(x) iff. f(x)', () => {
    fc.assert(fc.property(fc.integer(), fc.func(fc.boolean()), (i, f) => {
      if (f(i)) {
        assertSome(some(i).filter(f), i);
      } else {
        assertNone(some(i).filter(f));
      }
    }));
  })


it('fails for none vs some', () => {
    fc.assert(fc.property(fc.nat(), (n) => {
      assert.throw(() => {
        assertOptional(Optional.none(), Optional.some(n));
      });
    }));
  })


it('fails for some vs none', () => {
    fc.assert(fc.property(fc.nat(), (n) => {
      assert.throw(() => {
        assertOptional(Optional.some(n), Optional.none());
      });
    }));
  })


it('fails when some values are different', () => {
    fc.assert(fc.property(fc.nat(), (n) => {
      assert.throw(() => {
        assertOptional(Optional.some(n), Optional.some(n + 1));
      });
    }));
  })


it('passes for identical somes', () => {
    fc.assert(fc.property(fc.nat(), (n) => {
      assertOptional(Optional.some(n), Optional.some(n));
    }));
  })


it('passes for identical some arrays', () => {
    fc.assert(fc.property(fc.array(fc.nat()), (n) => {
      assertOptional(Optional.some(n), Optional.some(n));
    }));
  })


it('Checking that creating a namespace (forge) from an obj will enable that value to be retrieved by resolving (path)', () => {
    fc.assert(fc.property(
      // NOTE: This value is being modified, so it cannot be shrunk.
      fc.dictionary(fc.asciiString(),
        // We want to make sure every path in the object is an object
        // also, because that is a limitation of forge.
        fc.dictionary(fc.asciiString(),
          fc.dictionary(fc.asciiString(), fc.constant({}))
        )
      ),
      fc.array(fc.asciiString(1, 30), 1, 40),
      fc.asciiString(1, 30),
      fc.asciiString(1, 30),
      (dict, parts, field, newValue) => {
        const created = Resolve.forge(parts, dict);
        created[field] = newValue;
        const resolved = Resolve.path(parts.concat([ field ]), dict);
        assert.deepEqual(resolved, newValue);
      }
    ), { endOnFailure: true });
  })


it('map id obj = obj', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), (obj) => {
      const actual = Obj.map(obj, Fun.identity);
      assert.deepEqual(actual, obj);
    }));
  })


it('map constant obj means that values(obj) are all the constant', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), fc.integer(), (obj, x) => {
      const output = Obj.map(obj, Fun.constant(x));
      const values = Obj.values(output);
      return Arr.forall(values, (v) => v === x);
    }));
  })


it('tupleMap obj (x, i) -> { k: i, v: x }', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), (obj) => {
      const output = Obj.tupleMap(obj, (x, i) => ({ k: i, v: x }));
      assert.deepEqual(output, obj);
    }));
  })


it('mapToArray is symmetric with tupleMap', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), (obj) => {
      const array = Obj.mapToArray(obj, (x, i) => ({ k: i, v: x }));

      const aKeys = Arr.map(array, (x) => x.k);
      const aValues = Arr.map(array, (x) => x.v);

      const keys = Obj.keys(obj);
      const values = Obj.values(obj);

      assert.deepEqual(Arr.sort(aKeys), Arr.sort(keys));
      assert.deepEqual(Arr.sort(aValues), Arr.sort(values));
    }));
  })


it('single key/value', () => {
    fc.assert(fc.property(
      fc.string(),
      fc.json(),
      (k: string, v: any) => {
        const o = { [k]: v };
        assert.isFalse(Obj.isEmpty(o));
      }
    ));
  })


it('the value found by find always passes predicate', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.json()),
      fc.func(fc.boolean()),
      (obj, pred) => {
        // It looks like the way that fc.fun works is it cares about all of its arguments, so therefore
        // we have to only pass in one if we want it to be deterministic. Just an assumption
        const value = Obj.find(obj, (v) => {
          return pred(v);
        });
        return value.fold(() => {
          const values = Obj.values(obj);
          return !Arr.exists(values, (v) => {
            return pred(v);
          });
        }, (v) => {
          return pred(v);
        });
      }
    ));
  })


it('If predicate is always false, then find is always none', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.json()),
      (obj) => {
        const value = Obj.find(obj, Fun.never);
        return value.isNone();
      }
    ));
  })


it('If object is empty, find is always none', () => {
    fc.assert(fc.property(
      fc.func(fc.boolean()),
      (pred) => {
        const value = Obj.find({}, pred);
        return value.isNone();
      }
    ));
  })


it('If predicate is always true, then value is always the some(first), or none if dict is empty', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.json()),
      (obj) => {
        const value = Obj.find(obj, Fun.always);
        // No order is specified, so we cannot know what "first" is
        return Obj.keys(obj).length === 0 ? value.isNone() : true;
      }
    ));
  })


it('Each + set should equal the same object', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.json()),
      (obj) => {
        const values: Record<string, string> = {};
        const output = Obj.each(obj, (x, i) => {
          values[i] = x;
        });
        assert.deepEqual(values, obj);
        assert.isUndefined(output);
      }
    ));
  })


it('Merged with identity on left', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.merge({}, obj), obj);
    }));
  })


it('Merged with identity on right', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.merge(obj, {}), obj);
    }));
  })


it('Merged with itself is itself', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.merge(obj, obj), obj);
    }));
  })


it('Merge(a, Merge(b, c)) === Merge(Merge(a, b), c)', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), (a, b, c) => {
      const one = Merger.merge(a, Merger.merge(b, c));
      const other = Merger.merge(Merger.merge(a, b), c);
      assert.deepEqual(other, one);
    }));
  })


it('Merge(a, b) contains all the keys of b', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), (a, b) => {
      const output = Merger.merge(a, b);
      const keys = Obj.keys(b);
      const oKeys = Obj.keys(output);
      return Arr.forall(keys, (k) => {
        return Arr.contains(oKeys, k);
      });
    }));
  })


it('Deep-merged with identity on left', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.deepMerge({}, obj), obj);
    }));
  })


it('Deep-merged with identity on right', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.deepMerge(obj, {}), obj);
    }));
  })


it('Deep-merged with itself is itself', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), (obj) => {
      assert.deepEqual(Merger.deepMerge(obj, obj), obj);
    }));
  })


it('Deep-merge(a, Deep-merge(b, c)) === Deep-merge(Deep-merge(a, b), c)', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), (a, b, c) => {
      const one = Merger.merge(a, Merger.merge(b, c));
      const other = Merger.merge(Merger.merge(a, b), c);
      assert.deepEqual(other, one);
    }));
  })


it('Deep-merge(a, b) contains all the keys of b', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.json()), fc.dictionary(fc.asciiString(), fc.json()), (a, b) => {
      const output = Merger.deepMerge(a, b);
      const keys = Obj.keys(b);
      const oKeys = Obj.keys(output);
      return Arr.forall(keys, (k) => {
        return Arr.contains(oKeys, k);
      });
    }));
  })


it('Check that if the filter always returns false, then everything is in "f"', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.string(1, 40)),
      (obj) => {
        const output = Obj.bifilter(obj, Fun.never);
        assert.lengthOf(Obj.keys(output.f), Obj.keys(obj).length);
        assert.lengthOf(Obj.keys(output.t), 0);
        return true;
      }
    ));
  })


it('Check that if the filter always returns true, then everything is in "t"', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(), fc.string(1, 40)),
      (obj) => {
        const output = Obj.bifilter(obj, Fun.always);
        assert.lengthOf(Obj.keys(output.f), 0);
        assert.lengthOf(Obj.keys(output.t), Obj.keys(obj).length);
        return true;
      }
    ));
  })


it('Check that everything in f fails predicate and everything in t passes predicate', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(1, 30), fc.integer()),
      (obj) => {
        const predicate = (x: number) => x % 2 === 0;
        const output = Obj.bifilter(obj, predicate);

        const matches = (k: string) => predicate(obj[k]);

        const falseKeys = Obj.keys(output.f);
        const trueKeys = Obj.keys(output.t);

        assert.isFalse(Arr.exists(falseKeys, matches));
        assert.isTrue(Arr.forall(trueKeys, matches));
      }
    ));
  })


it('should have an adjustment of delta, or be the min or max', () => {
    fc.assert(fc.property(
      fc.nat(),
      fc.integer(),
      fc.nat(),
      fc.nat(),
      (value, delta, min, range) => {
        const max = min + range;
        const actual = Num.cycleBy(value, delta, min, max);
        return (actual - value) === delta || actual === min || actual === max;
      }
    ));
  })


it('0 has no effect', () => {
    fc.assert(fc.property(fc.nat(), fc.nat(), (value, delta) => {
      const actual = Num.cycleBy(value, 0, value, value + delta);
      assert.equal(actual, value);
    }));
  })


it('delta is max', () => {
    fc.assert(fc.property(fc.nat(), fc.nat(), (value, delta) => {
      const max = value + delta;
      const actual = Num.cycleBy(value, delta, value, max);
      assert.equal(actual, max);
    }));
  })


it('Num.clamp', () => {
    fc.assert(fc.property(
      fc.nat(1000),
      fc.nat(1000),
      fc.nat(1000),
      (a, b, c) => {
        const low = a;
        const med = low + b;
        const high = med + c;
        // low <= med <= high
        assert.equal(Num.clamp(med, low, high), med);
        assert.equal(Num.clamp(med, low, high), med);
        assert.equal(Num.clamp(high, low, med), med);
      }));
  })


it('mirrors bind', () => {
      const arbInner = fc.tuple(fc.boolean(), fc.string()).map(([ isJust, value ]) => {
        if (isJust) {
          return Maybes.just(value);
        } else {
          return Maybes.nothing<string>();
        }
      });
      const arbNested = fc.tuple(fc.boolean(), arbInner).map(([ isJust, value ]) => {
        if (isJust) {
          return Maybes.just(value);
        } else {
          return Maybes.nothing<Maybe<string>>();
        }
      });
      fc.assert(fc.property(arbNested, (maybe) => {
        const flattened = Maybes.flatten(maybe);
        const bound = Maybes.bind<Maybe<string>, string>(Fun.identity)(maybe);
        assert.deepEqual(flattened, bound);
      }));
    })


it('allows creating "Just" maybes', () => {
    fc.assert(fc.property(fc.anything(), (thing) => {
      const item = Maybes.just(thing);
      assert.isTrue(Maybes.isJust(item));
      assert.isFalse(Maybes.isNothing(item));
    }));
  })


it('allows folding on "Just" maybes', () => {
    // Simple value, complex assertions
    Fun.pipe(
      Maybes.just('test'),
      Maybes.fold(
        Fun.die('Should call other branch'),
        (test) => {
          assert.equal(test, 'test');
          return 'other-test';
        }
      ),
      (result) => assert.equal(result, 'other-test')
    );

    // Complex values, simple assertions
    fc.assert(fc.property(fc.anything(), (thing) => {
      Fun.pipe(
        Maybes.just(thing),
        Maybes.fold(Fun.die('Should call other branch'), Fun.noop)
      );
    }));
  })


it('gets the value on "Just"', () => {
      fc.assert(fc.property(fc.string(), (s) => {
        const val = Fun.pipe(
          Maybes.just(s),
          Maybes.getOrDie
        );
        assert.equal(val, s);
      }));
    })


it('is always false for "Nothing"', () => {
      fc.assert(fc.property(fc.anything(), (thing) => {
        const matches = Fun.pipe(
          Maybes.nothing(),
          Maybes.is(thing)
        );

        assert.isFalse(matches);
      }));
    })


it('is always false for "Nothing" with explicit comparator', () => {
      fc.assert(fc.property(fc.anything(), (thing) => {
        const matches = Fun.pipe(
          Maybes.nothing(),
          Maybes.is(thing, Fun.die('should not be called'))
        );

        assert.isFalse(matches);
      }));
    })


it('is correct for "Just"', () => {
      fc.assert(fc.property(fc.string(), (thing) => {
        const matches = Fun.pipe(
          Maybes.just(thing),
          Maybes.is(thing)
        );

        assert.isTrue(matches);
      }));

      fc.assert(fc.property(fc.string(), (thing) => {
        const matches = Fun.pipe(
          Maybes.just(thing),
          Maybes.is(thing + 'foo')
        );

        assert.isFalse(matches);
      }));
    })


it('is correct for "Just" with an explicit comparator', () => {

      // Just an example data type that doesn't have a simple equality
      const thunkEq = (a: () => string, b: () => string): boolean => a() === b();

      fc.assert(fc.property(fc.string(), (thing) => {
        const matches = Fun.pipe(
          Maybes.just(Fun.constant(thing)),
          Maybes.is(Fun.constant(thing), thunkEq)
        );

        assert.isTrue(matches);
      }));

      fc.assert(fc.property(fc.string(), (thing) => {
        const matches = Fun.pipe(
          Maybes.just(Fun.constant(thing)),
          Maybes.is(() => thing + 'foo', thunkEq)
        );

        assert.isFalse(matches);
      }));
    })


it('is false for one "Nothing"', () => {
      fc.assert(fc.property(fc.anything(), (thing) => {
        assert.isFalse(Maybes.equals(Maybes.nothing(), Maybes.just(thing)));
        assert.isFalse(Maybes.equals(Maybes.just(thing), Maybes.nothing()));
      }));
    })


it('compares normally for two "Just"s', () => {
      const different = fc.tuple(fc.anything(), fc.anything()).filter(([ lhs, rhs ]) => lhs !== rhs);
      fc.assert(fc.property(different, ([ lhs, rhs ]) => {
        assert.isFalse(Maybes.equals(Maybes.just(lhs), Maybes.just(rhs)));
      }));

      const theSame = fc.string().map((x) => [ x, x ]);
      fc.assert(fc.property(theSame, ([ lhs, rhs ]) => {
        assert.isTrue(Maybes.equals(Maybes.just(lhs), Maybes.just(rhs)));
      }));
    })


it('is false for one "Nothing"', () => {
      fc.assert(fc.property(fc.anything(), (thing) => {
        assert.isFalse(Maybes.equals(Maybes.nothing(), Maybes.just(thing), unusedComparator));
        assert.isFalse(Maybes.equals(Maybes.just(thing), Maybes.nothing(), unusedComparator));
      }));
    })


it('Thunk.cached counter', () => {
    fc.assert(fc.property(fc.json(), fc.func(fc.json()), fc.json(), (a, f, b) => {
      let counter = 0;
      const thunk = Thunk.cached((x) => {
        counter++;
        return {
          counter,
          output: f(x)
        };
      });
      const value = thunk(a);
      const other = thunk(b);
      assert.deepEqual(other, value);
    }));
  })


it('Check compose :: compose(f, g)(x) = f(g(x))', () => {
    fc.assert(fc.property(fc.string(), fc.func(fc.string()), fc.func(fc.string()), (x, f, g) => {
      const h = Fun.compose(f, g);
      assert.deepEqual(h(x), f(g(x)));
    }));
  })


it('Check compose1 :: compose1(f, g)(x) = f(g(x))', () => {
    fc.assert(fc.property(fc.string(), fc.func(fc.string()), fc.func(fc.string()), (x, f, g) => {
      const h = Fun.compose1(f, g);
      assert.deepEqual(h(x), f(g(x)));
    }));
  })


it('Check constant :: constant(a)() === a', () => {
    fc.assert(fc.property(fc.json(), (json) => {
      assert.deepEqual(Fun.constant(json)(), json);
    }));
  })


it('Check identity :: identity(a) === a', () => {
    fc.assert(fc.property(fc.json(), (json) => {
      assert.deepEqual(Fun.identity(json), json);
    }));
  })


it('Check always :: f(x) === true', () => {
    fc.assert(fc.property(fc.json(), (json) => {
      assert.isTrue(Fun.always(json));
    }));
  })


it('Check never :: f(x) === false', () => {
    fc.assert(fc.property(fc.json(), (json) => {
      assert.isFalse(Fun.never(json));
    }));
  })


it('Check curry', () => {
    fc.assert(fc.property(fc.json(), fc.json(), fc.json(), fc.json(), (a, b, c, d) => {
      const f = (a: string, b: string, c: string, d: string) => [ a, b, c, d ];

      assert.deepEqual([ a, b, c, d ], Fun.curry(f, a)(b, c, d));
      assert.deepEqual([ a, b, c, d ], Fun.curry(f, a, b)(c, d));
      assert.deepEqual([ a, b, c, d ], Fun.curry(f, a, b, c)(d));
      assert.deepEqual([ a, b, c, d ], Fun.curry(f, a, b, c, d)());
    }));
  })


it('Check not :: not(f(x)) === !f(x)', () => {
    fc.assert(fc.property(fc.json(), fc.func(fc.boolean()), (x, f) => {
      const g = Fun.not(f);
      assert.deepEqual(!g(x), f(x));
    }));
  })


it('Check not :: not(not(f(x))) === f(x)', () => {
    fc.assert(fc.property(fc.json(), fc.func(fc.boolean()), (x, f) => {
      const g = Fun.not(Fun.not(f));
      assert.deepEqual(g(x), f(x));
    }));
  })


it('Check apply :: apply(constant(a)) === a', () => {
    fc.assert(fc.property(fc.json(), (x) => {
      assert.deepEqual(Fun.apply(Fun.constant(x)), x);
    }));
  })


it('Check call :: apply(constant(a)) === undefined', () => {
    fc.assert(fc.property(fc.json(), (x) => {
      let hack: any = null;
      const output = Fun.call(() => {
        hack = x;
      });

      assert.isUndefined(output);
      assert.deepEqual(hack, x);
    }));
  })


it('Check that values should be empty and  errors should be all if we only generate errors', () => {
    fc.assert(fc.property(
      fc.array(arbResultError(fc.integer())),
      (resErrors) => {
        const actual = Results.partition(resErrors);
        if (actual.values.length !== 0) {
          assert.fail('Values length should be 0');
        } else if (resErrors.length !== actual.errors.length) {
          assert.fail('Errors length should be ' + resErrors.length);
        }
        return true;
      }
    ));
  })


it('Check that errors should be empty and values should be all if we only generate values', () => {
    fc.assert(fc.property(
      fc.array(arbResultValue(fc.integer())),
      (resValues) => {
        const actual = Results.partition(resValues);
        if (actual.errors.length !== 0) {
          assert.fail('Errors length should be 0');
        } else if (resValues.length !== actual.values.length) {
          assert.fail('Values length should be ' + resValues.length);
        }
        return true;
      }
    ));
  })


it('Check that the total number of values and errors matches the input size', () => {
    fc.assert(fc.property(
      fc.array(arbResult(fc.integer(), fc.string())),
      (results) => {
        const actual = Results.partition(results);
        assert.equal(actual.errors.length + actual.values.length, results.length);
      }
    ));
  })


it('Check that two errors always equal comparison.bothErrors', () => {
    fc.assert(fc.property(
      arbResultError(fc.integer()),
      arbResultError(fc.integer()),
      (r1, r2) => {
        const comparison = Results.compare(r1, r2);
        return comparison.match({
          bothErrors: Fun.always,
          firstError: Fun.never,
          secondError: Fun.never,
          bothValues: Fun.never
        });
      }
    ));
  })


it('Check that error, value always equal comparison.firstError', () => {
    fc.assert(fc.property(
      arbResultError(fc.integer()),
      arbResultValue(fc.string()),
      (r1, r2) => {
        const comparison = Results.compare(r1, r2);
        return comparison.match({
          bothErrors: Fun.never,
          firstError: Fun.always,
          secondError: Fun.never,
          bothValues: Fun.never
        });
      }
    ));
  })


it('Check that value, error always equal comparison.secondError', () => {
    fc.assert(fc.property(
      arbResultValue(fc.integer()),
      arbResultError(fc.string()),
      (r1, r2) => {
        const comparison = Results.compare(r1, r2);
        return comparison.match({
          bothErrors: Fun.never,
          firstError: Fun.never,
          secondError: Fun.always,
          bothValues: Fun.never
        });
      }
    ));
  })


it('Check that value, value always equal comparison.bothValues', () => {
    fc.assert(fc.property(
      arbResultValue(fc.integer()),
      arbResultValue(fc.integer()),
      (r1, r2) => {
        const comparison = Results.compare(r1, r2);
        return comparison.match({
          bothErrors: Fun.never,
          firstError: Fun.never,
          secondError: Fun.never,
          bothValues: Fun.always
        });
      }
    ));
  })


it('Results.unite', () => {
    fc.assert(fc.property(fc.integer(), (a) => {
      assert.equal(Results.unite(Result.error<number, number>(a)), a);
      assert.equal(Results.unite(Result.value<number, number>(a)), a);
    }));
  })


it('Checking value.is(value.getOrDie()) === true', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      assert.isTrue(Results.is(res, res.getOrDie()));
    }));
  })


it('Checking value.isValue === true', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      assert.isTrue(res.isValue());
    }));
  })


it('Checking value.isError === false', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      assert.isFalse(res.isError());
    }));
  })


it('Checking value.getOr(v) === value.value', () => {
    fc.assert(fc.property(fc.integer(), (a) => {
      assert.equal(Result.value(a).getOr(a), a);
    }));
  })


it('Checking value.getOrDie() does not throw', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      res.getOrDie();
    }));
  })


it('Checking value.or(oValue) === value', () => {
    fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => {
      assertResult(Result.value(a).or(Result.value(b)), Result.value(a));
    }));
  })


it('Checking error.or(value) === value', () => {
    fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => {
      assertResult(Result.error(a).or(Result.value(b)), Result.value(b));
    }));
  })


it('Checking value.orThunk(die) does not throw', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      res.orThunk(Fun.die('dies'));
    }));
  })


it('Checking value.fold(die, id) === value.getOrDie()', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      const actual = res.getOrDie();
      assert.equal(res.fold(Fun.die('should not get here'), Fun.identity), actual);
    }));
  })


it('Checking value.map(f) === f(value.getOrDie())', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), fc.func(fc.json()), (res, f) => {
      assert.equal(f(res.getOrDie()), res.map(f).getOrDie());
    }));
  })


it('Checking value.each(f) === undefined', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), fc.func(fc.json()), (res, f) => {
      const actual = res.each(f);
      assert.isUndefined(actual);
    }));
  })


it('Given f :: s -> RV, checking value.bind(f).getOrDie() === f(value.getOrDie()).getOrDie()', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), fc.func(arbResultValue(fc.integer())), (res, f) => {
      assert.equal(f(res.getOrDie()).getOrDie(), res.bind(f).getOrDie());
    }));
  })


it('Given f :: s -> RE, checking value.bind(f).fold(id, die) === f(value.getOrDie()).fold(id, die)', () => {
    fc.assert(fc.property(arbResultValue<number, number>(fc.integer()), fc.func(arbResultError<number, number>(fc.integer())), (res, f) => {
      const toErrString = (r: Result<number, number>) => r.fold(Fun.identity, Fun.die('Not a Result.error'));
      assert.equal(toErrString(f(res.getOrDie())), toErrString(res.bind(f)));
    }));
  })


it('Checking value.forall is true iff. f(value.getOrDie() === true)', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), fc.func(fc.boolean()), (res, f) => {
      assert.equal(res.forall(f), f(res.getOrDie()));
    }));
  })


it('Checking value.exists is true iff. f(value.getOrDie() === true)', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), fc.func(fc.boolean()), (res, f) => {
      assert.equal(res.exists(f), f(res.getOrDie()));
    }));
  })


it('Checking value.toOptional is always Optional.some(value.getOrDie())', () => {
    fc.assert(fc.property(arbResultValue(fc.integer()), (res) => {
      assert.equal(res.toOptional().getOrDie(), res.getOrDie());
    }));
  })


it('error.is === false', () => {
    fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
      assert.isFalse(Results.is(Result.error(s), i));
    }));
  })


it('error.isValue === false', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), (res) => {
      assert.isFalse(res.isValue());
    }));
  })


it('error.isError === true', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), (res) => {
      assert.isTrue(res.isError());
    }));
  })


it('error.getOr(v) === v', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), fc.json(), (res, json) => {
      assert.deepEqual(res.getOr(json), json);
    }));
  })


it('error.getOrDie() always throws', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), (res) => {
      assert.throws(() => {
        res.getOrDie();
      });
    }));
  })


it('error.or(oValue) === oValue', () => {
    fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
      assertResult(Result.error<number, string>(s).or(Result.value(i)), Result.value(i));
    }));
  })


it('error.orThunk(_ -> v) === v', () => {
    fc.assert(fc.property(fc.integer(), fc.string(), (i, s) => {
      assertResult(Result.error<number, string>(s).orThunk(() => Result.value(i)), Result.value(i));
    }));
  })


it('error.fold(_ -> x, die) === x', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), fc.json(), (res, json) => {
      const actual = res.fold(Fun.constant(json), Fun.die('Should not die'));
      assert.deepEqual(actual, json);
    }));
  })


it('error.map returns an error', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      assertResult(Result.error(i).map(Fun.die('should not be called')), Result.error(i));
    }));
  })


it('error.mapError(f) === f(error)', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      const f = (x: number) => x % 3;
      assertResult(Result.error(i).mapError(f), Result.error(f(i)));
    }));
  })


it('error.each returns undefined', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), (res) => {
      const actual = res.each(Fun.die('should not be called'));
      assert.isUndefined(actual);
    }));
  })


it('Given f :: s -> RV, error.bind(f) === error', () => {
    fc.assert(fc.property(arbResultError<number, number>(fc.integer()), fc.func(arbResultValue<number, number>(fc.integer())), (res, f) => {
      const actual = res.bind(f);
      assert.deepEqual(getErrorOrDie(actual), getErrorOrDie(res));
    }));
  })


it('Given f :: s -> RE, error.bind(f) === error', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), fc.func(arbResultError(fc.integer())), (res, f) => {
      const actual = res.bind(f);
      assert.deepEqual(getErrorOrDie(actual), getErrorOrDie(res));
    }));
  })


it('error.forall === true', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), fc.func(fc.boolean()), (res, f) => {
      assert.isTrue(res.forall(f));
    }));
  })


it('error.exists === false', () => {
    fc.assert(fc.property(fc.integer(), (i) => {
      assert.isFalse(Result.error<unknown, number>(i).exists(Fun.die('should not be called')));
    }));
  })


it('error.toOptional is always none', () => {
    fc.assert(fc.property(arbResultError(fc.integer()), (res) => {
      assertNone(res.toOptional());
    }));
  })


it('should not generate identical IDs', () => {
    const arbId = fc.string(1, 30).map(Id.generate);
    fc.assert(fc.property(arbId, arbId, (id1, id2) => id1 !== id2));
  })


it('pure', () =>
    fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
      LazyValue.pure(i).get((v) => {
        eqAsync('LazyValue.pure', i, v, reject);
        resolve();
      });
    }))))


it('pure, map', () =>
    fc.assert(fc.asyncProperty(fc.integer(), fc.func(fc.string()), (i, f) => new Promise((resolve, reject) => {
      LazyValue.pure(i).map(f).get((v) => {
        eqAsync('LazyValue.map', f(i), v, reject);
        resolve();
      });
    }))))


it('delayed, map', () =>
    fc.assert(fc.asyncProperty(fc.integer(), fc.func(fc.string()), (i, f) => new Promise((resolve, reject) => {
      LazyValue.nu((c) => {
        setTimeout(() => {
          c(i);
        }, 2);
      }).map(f).get((v) => {
        eqAsync('LazyValue.map', f(i), v, reject);
        resolve();
      });
    }))))


it('parallel spec', () => fc.assert(fc.asyncProperty(fc.array(fc.integer(), 0, 20), (vals) => new Promise((resolve, reject) => {
    const lazyVals = Arr.map(vals, LazyValue.pure);
    LazyValues.par(lazyVals).get((actual) => {
      eqAsync('pars', vals, actual, reject);
      resolve();
    });
  }))))


it('pure get', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) =>
    Future.pure(i).get((ii) => {
      eqAsync('pure get', i, ii, reject, tNumber);
      resolve();
    })))))


it('future soon get', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) =>
    Future.nu((cb) => setTimeout(() => cb(i), 3)).get((ii) => {
      eqAsync('get', i, ii, reject, tNumber);
      resolve();
    })))))


it('map', () => fc.assert(fc.asyncProperty(fc.integer(), fc.func(fc.string()), (i, f) => new Promise((resolve, reject) =>
    Future.pure(i).map(f).get((ii) => {
      eqAsync('get', f(i), ii, reject, tString);
      resolve();
    })))))


it('bind', () => fc.assert(fc.asyncProperty(fc.integer(), fc.func(fc.string()), (i, f) => new Promise((resolve, reject) =>
    Future.pure(i).bind(Fun.compose(Future.pure, f)).get((ii) => {
      eqAsync('get', f(i), ii, reject, tString);
      resolve();
    })))))


it('anonBind', () => fc.assert(fc.asyncProperty(fc.integer(), fc.string(), (i, s) => new Promise((resolve, reject) =>
    Future.pure(i).anonBind(Future.pure(s)).get((ii) => {
      eqAsync('get', s, ii, reject, tString);
      resolve();
    })))))


it('parallel spec', () => fc.assert(fc.asyncProperty(fc.array(fc.tuple(fc.integer(1, 10), fc.integer())), (tuples) => new Promise((resolve, reject) =>
    Futures.par(Arr.map(tuples, ([ timeout, value ]) => Future.nu((cb) => setTimeout(() => cb(value), timeout)))).get((ii) => {
      eqAsync('pars', tuples.map(([ _, i ]) => i), ii, reject, tArray(tNumber));
      resolve();
    }))))
  )


it('mapM spec', () =>
    fc.assert(fc.asyncProperty(fc.array(fc.tuple(fc.integer(1, 10), fc.integer())), (tuples) => new Promise((resolve, reject) =>
      Futures.mapM(tuples, ([ timeout, value ]) => Future.nu((cb) => setTimeout(() => cb(value), timeout))).get((ii) => {
        eqAsync('pars', tuples.map(([ _, i ]) => i), ii, reject, tArray(tNumber));
        resolve();
      }))))
  )


it('nu', () => fc.assert(fc.asyncProperty(arbResult(fc.integer(), fc.integer()), (r) => new Promise((resolve, reject) => {
    FutureResult.nu((completer) => {
      completer(r);
    }).get((ii) => {
      eqAsync('eq', r, ii, reject, tResult());
      resolve();
    });
  }))))


it('fromFuture', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.fromFuture(Future.pure(i)).get((ii) => {
      eqAsync('eq', Result.value(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('wrap get', () => fc.assert(fc.asyncProperty(arbResult(fc.integer(), fc.integer()), (r) => new Promise((resolve, reject) => {
    FutureResult.wrap(Future.pure(r)).get((ii) => {
      eqAsync('eq', r, ii, reject, tResult());
      resolve();
    });
  }))))


it('fromResult get', () => fc.assert(fc.asyncProperty(arbResult(fc.integer(), fc.integer()), (r) => new Promise((resolve, reject) => {
    FutureResult.fromResult(r).get((ii) => {
      eqAsync('eq', r, ii, reject, tResult());
      resolve();
    });
  }))))


it('pure get', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.pure<number, unknown>(i).get((ii) => {
      eqAsync('eq', Result.value(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('value get', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.value<number, unknown>(i).get((ii) => {
      eqAsync('eq', Result.value(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('error get', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.error<unknown, number>(i).get((ii) => {
      eqAsync('eq', Result.error(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('value mapResult', () => {
    const f = (x: number) => x + 3;
    return fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
      FutureResult.value(i).mapResult(f).get((ii) => {
        eqAsync('eq', Result.value(f(i)), ii, reject, tResult());
        resolve();
      });
    })));
  })


it('error mapResult', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.error(i).mapResult(Fun.die('should not be called')).get((ii) => {
      eqAsync('eq', Result.error(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('value mapError', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.value(i).mapError(Fun.die('should not be called')).get((ii) => {
      eqAsync('eq', Result.value(i), ii, reject, tResult());
      resolve();
    });
  }))))


it('err mapError', () => {
    const f = (x: number) => x + 3;
    return fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
      FutureResult.error(i).mapError(f).get((ii) => {
        eqAsync('eq', Result.error(f(i)), ii, reject, tResult());
        resolve();
      });
    })));
  })


it('value bindFuture value', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    const f = (x: number) => x % 4;
    FutureResult.value(i).bindFuture((x) => FutureResult.value(f(x))).get((actual) => {
      eqAsync('bind result', Result.value(f(i)), actual, reject, tResult(tNumber));
      resolve();
    });
  }))))


it('bindFuture: value bindFuture error', () => fc.assert(fc.asyncProperty(fc.integer(), fc.string(), (i, s) => new Promise((resolve, reject) => {
    FutureResult.value(i).bindFuture(() => FutureResult.error(s)).get((actual) => {
      eqAsync('bind result', Result.error(s), actual, reject, tResult(tNumber));
      resolve();
    });
  }))))


it('error bindFuture', () => fc.assert(fc.asyncProperty(fc.integer(), (i) => new Promise((resolve, reject) => {
    FutureResult.error(i).bindFuture<never>(Fun.die('should not be called')).get((actual) => {
      eqAsync('bind result', Result.error(i), actual, reject, tResult(tNumber));
      resolve();
    });
  }))))


it('returns matching keys and values', () => {
    fc.assert(fc.property(
      fc.array(fc.asciiString(1, 30)),
      (rawValues: string[]) => {
        const values = Unique.stringArray(rawValues);

        const keys = Arr.map(values, (v, i) => i);

        const output = Zip.zipToObject(keys, values);

        const oKeys = Obj.keys(output);
        assert.deepEqual(values.length, oKeys.length);

        assert.deepEqual(Arr.forall(oKeys, (oKey) => {
          const index = parseInt(oKey, 10);
          const expected = values[index];
          return output[oKey] === expected;
        }), true);
      }
    ));
  })


it('matches corresponding tuples', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.array(fc.integer()),
      (keys, values) => {
        if (keys.length !== values.length) {
          assert.throws(() => Zip.zipToTuples(keys, values));
        } else {
          const output = Zip.zipToTuples(keys, values);
          assert.equal(output.length, keys.length);
          assert.isTrue(Arr.forall(output, (x, i) => x.k === keys[i] && x.v === values[i]));
        }
      }
    ));
  })


it('Reversing twice is identity', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      assert.deepEqual(Arr.reverse(Arr.reverse(arr)), arr);
    }));
  })


it('reversing a one element array is identity', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (a) => {
      assert.deepEqual(Arr.reverse([ a ]), [ a ]);
    }));
  })


it('reverses 2 elements', () => {
    fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => {
      assert.deepEqual(Arr.reverse([ a, b ]), [ b, a ]);
    }));
  })


it('Check that if the filter always returns false, then everything is in "fail"', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      (arr) => {
        const output = Arr.partition(arr, Fun.never);
        assert.deepEqual(output.pass.length, 0);
        assert.deepEqual(output.fail, arr);
      }
    ));
  })


it('Check that if the filter always returns true, then everything is in "pass"', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      (arr) => {
        const output = Arr.partition(arr, Fun.always);
        assert.deepEqual(output.fail.length, 0);
        assert.deepEqual(output.pass, arr);
      }
    ));
  })


it('Check that everything in fail fails predicate and everything in pass passes predicate', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      (arr) => {
        const predicate = (x: number) => x % 3 === 0;
        const output = Arr.partition(arr, predicate);
        return Arr.forall(output.fail, (x) => !predicate(x)) && Arr.forall(output.pass, predicate);
      }
    ));
  })


it('inductive case', () => {
    fc.assert(fc.property(
      fc.dictionary(fc.asciiString(1, 30), fc.integer()),
      fc.asciiString(1, 30),
      fc.integer(),
      (obj, k, v) => {
        const objWithoutK = Obj.filter(obj, (x, i) => i !== k);
        assert.deepEqual(Obj.size({ [k]: v, ...objWithoutK }), Obj.size(objWithoutK) + 1);
      }), {
      numRuns: 5000
    });
  })


it('only returns elements that are in the input', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), (obj) => {
      const keys = Obj.keys(obj);
      return Arr.forall(keys, (k) => obj.hasOwnProperty(k));
    }));
  })


it('filter const true is identity', () => {
    fc.assert(fc.property(fc.dictionary(fc.asciiString(), fc.integer()), (obj) => {
      assert.deepEqual(Obj.filter(obj, Fun.always), obj);
    }));
  })


it('Length of interspersed = len(arr) + len(arr)-1', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.integer(),
      (arr, delimiter) => {
        const actual = Jam.intersperse(arr, delimiter);
        const expected = arr.length === 0 ? 0 : arr.length * 2 - 1;
        assert.deepEqual(actual.length, expected);
      }
    ));
  })


it('Every odd element matches delimiter', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.integer(),
      (arr, delimiter) => {
        const actual = Jam.intersperse(arr, delimiter);
        return Arr.forall(actual, (x, i) => i % 2 === 1 ? x === delimiter : true);
      }
    ));
  })


it('Filtering out delimiters (assuming different type to array to avoid removing original array) should equal original', () => {
    fc.assert(fc.property(
      fc.array(fc.nat()),
      arbNegativeInteger(),
      (arr, delimiter) => {
        const actual = Jam.intersperse(arr, delimiter);
        const filtered = Arr.filter(actual, (a) => a !== delimiter);
        assert.deepEqual(filtered, arr);
      }
    ));
  })


it('finds element in middle of array', () => {
    fc.assert(fc.property(fc.array(fc.nat()), arbNegativeInteger(), fc.array(fc.nat()), (prefix, element, suffix) => {
      const arr = prefix.concat([ element ]).concat(suffix);
      assertSome(Arr.indexOf(arr, element), prefix.length);
    }));
  })


it('Adjacent groups have different hashes, and everything in a group has the same hash', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.func(fc.asciiString()),
      (xs, f) => {
        const groups = Arr.groupBy(xs, (x) => f(x));

        /* Properties about groups
         * 1. No two adjacent groups can have the same g(..) value
         * 2. Each group must have the same g(..) value
         */

        const hasEmptyGroups = Arr.exists(groups, (g) => g.length === 0);

        if (hasEmptyGroups) {
          assert.fail('Should not have empty groups');
        }
        // No consecutive groups should have the same result of g.
        const values = Arr.map(groups, (group) => {
          const first = f(group[0]);
          const mapped = Arr.map(group, (g) => f(g));

          const isSame = Arr.forall(mapped, (m) => m === first);
          if (!isSame) {
            assert.fail('Not everything in a group has the same g(..) value');
          }
          return first;
        });

        const hasSameGroup = Arr.exists(values, (v, i) => i > 0 ? values[i - 1] === values[i] : false);

        if (hasSameGroup) {
          assert.fail('A group is next to another group with the same g(..) value');
        }
        return true;
      }
    ));
  })


it('Flattening groups equals the original array', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.func(fc.string()),
      (xs, f) => {
        const groups = Arr.groupBy(xs, (x) => f(x));

        const output = Arr.flatten(groups);
        assert.deepEqual(output, xs);
      }
    ));
  })


it('forall of a non-empty array with a predicate that always returns false is false', () => {
    fc.assert(fc.property(
      fc.array(fc.integer(), 1, 30),
      (xs) => {
        assert.isFalse(Arr.forall(xs, Fun.never));
      }
    ));
  })


it('forall of a non-empty array with a predicate that always returns true is true', () => {
    fc.assert(fc.property(
      fc.array(fc.integer(), 1, 30),
      (xs) => {
        assert.isTrue(Arr.forall(xs, Fun.always));
      }
    ));
  })


it('foldl concat [ ] xs === reverse(xs)', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      (arr) => {
        const output = Arr.foldl(arr, (b: number[], a: number) => [ a ].concat(b), []);
        assert.deepEqual(output, Arr.reverse(arr));
      }
    ));
  })


it('foldr concat [ ] xs === xs', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      (arr) => {
        const output = Arr.foldr(arr, (b: number[], a: number) => [ a ].concat(b), []);
        assert.deepEqual(output, arr);
      }
    ));
  })


it('foldr concat ys xs === xs ++ ys', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.array(fc.integer()),
      (xs, ys) => {
        const output = Arr.foldr(xs, (b, a) => [ a ].concat(b), ys);
        assert.deepEqual(output, xs.concat(ys));
      }
    ));
  })


it('foldl concat ys xs === reverse(xs) ++ ys', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.array(fc.integer()),
      (xs, ys) => {
        const output = Arr.foldl(xs, (b, a) => [ a ].concat(b), ys);
        assert.deepEqual(output, Arr.reverse(xs).concat(ys));
      }
    ));
  })


it('is consistent with chunking', () => {
    fc.assert(fc.property(
      fc.array(fc.integer()),
      fc.integer(1, 5),
      (arr, chunkSize) => {
        const chunks = Arr.chunk(arr, chunkSize);
        const bound = Arr.flatten(chunks);
        assert.deepEqual(bound, arr);
      }
    ));
  })


it('wrap then flatten array is identity', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      assert.deepEqual(arr, Arr.flatten(Arr.pure(arr)));
    }));
  })


it('mapping pure then flattening array is identity', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      assert.deepEqual(arr, Arr.flatten(Arr.map(arr, Arr.pure)));
    }));
  })


it('flattening two lists === concat', () => {
    fc.assert(fc.property(fc.array(fc.integer()), fc.array(fc.integer()), (xs, ys) => {
      assert.deepEqual(xs.concat(ys), Arr.flatten([ xs, ys ]));
    }));
  })


describe('finds element in middle of array', () => {
    fc.assert(fc.property(fc.array(fc.nat()), arbNegativeInteger(), fc.array(fc.nat()), (prefix, element, suffix) => {
      const arr = [ ...prefix, element, ...suffix ];
      assertSome(
        Arr.findIndex(arr, (x) => x === element),
        prefix.length
      );
    }));
  })


it('finds elements that pass the predicate', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      const pred = (x: number) => x % 3 === 0;
      assert.isTrue(Arr.findIndex(arr, pred).forall((x) => pred(arr[x])));
    }));
  })


it('returns none if predicate always returns false', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      assertNone(Arr.findIndex(arr, Fun.never));
    }));
  })


it('is consistent with find', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      const pred = (x: number) => x % 5 === 0;
      assertOptional(Arr.findIndex(arr, pred).map((x) => arr[x]), Arr.find(arr, pred));
    }));
  })


it('is consistent with exists', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
      const pred = (x: number) => x % 6 === 0;
      assert.equal(Arr.findIndex(arr, pred).isSome(), Arr.exists(arr, pred));
    }));
  })


it('Element exists in middle of array', () => {
    fc.assert(fc.property(fc.array(fc.integer()), fc.integer(), fc.array(fc.integer()), (prefix, element, suffix) => {
      const arr2 = Arr.flatten([ prefix, [ element ], suffix ]);
      assert.isTrue(Arr.exists(arr2, eqc(element)));
    }));
  })


it('Element exists in singleton array of itself', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (i) => {
      assert.isTrue(Arr.exists([ i ], eqc(i)));
    }));
  })


it('Element does not exist in empty array', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (i) => {
      assert.isFalse(Arr.exists([], eqc(i)));
    }));
  })


it('Element not found when predicate always returns false', () => {
    fc.assert(fc.property(fc.array(fc.integer()), (arr) => !Arr.exists(arr, never)));
  })


it('Element exists in non-empty array when predicate always returns true', () => {
    fc.assert(fc.property(fc.array(fc.integer()), fc.integer(), (xs, x) => {
      const arr = Arr.flatten([ xs, [ x ]]);
      assert.isTrue(Arr.exists(arr, always));
    }));
  })


it('binding an array of empty arrays with identity equals an empty array', () => {
    fc.assert(fc.property(fc.array(fc.constant<number[]>([])), (arr) => {
      assert.deepEqual(Arr.bind(arr, Fun.identity), []);
    }));
  })

Summary: Binding an array of empty arrays with the identity function results in an empty array.


it('bind (pure .) is map', () => {
    fc.assert(fc.property(fc.array(fc.integer()), fc.integer(), (arr, j) => {
      const f = (x: number) => x + j;
      assert.deepEqual(Arr.bind(arr, Fun.compose(Arr.pure, f)), Arr.map(arr, f));
    }));
  })

Summary: Applying `bind` with `pure` composed is equivalent to `map`.


it('obeys right identity law', () => {
      fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
        assert.deepEqual(Arr.bind(arr, Arr.pure), arr);
      }));
    })

Summary: `Arr.bind` followed by `Arr.pure` returns the original array, demonstrating the right identity law.


it('never turns Just to Nothing (or vice versa)', () => {
    const mapper = Maybes.map(Fun.identity);

    const nothing = mapper(Maybes.nothing());
    assert.isTrue(Maybes.isNothing(nothing));

    fc.assert(fc.property(fc.anything(), (thing) => {
      const just = mapper(Maybes.just(thing));
      assert.isTrue(Maybes.isJust(just));
    }));
  })


it('executorLzReceiveOption', async () => {
        // sanity check that generated options are the same with value as 0 and not provided.
        expect(Options.newOptions().addExecutorLzReceiveOption(1, 0)).toEqual(
            Options.newOptions().addExecutorLzReceiveOption(1)
        )
        await fc.assert(
            fc.asyncProperty(
                omnicounterIncrementTypeArbitrary,
                gasLimitArbitrary,
                nativeDropArbitrary,

                // Test the generation and submission of arbitrary LZ_RECEIVE Options.  The transaction should succeed,
                // and the options from the transaction receipt logs should match the generated input.
                async (type, gasLimit, nativeDrop) => {
                    const options = Options.newOptions().addExecutorLzReceiveOption(gasLimit, nativeDrop)
                    const packetSentEvents = await incrementAndReturnLogs(type, options)
                    expect(packetSentEvents).toEqual([
                        expect.objectContaining({
                            args: expect.objectContaining({
                                options: expect.toEqualCaseInsensitive(options.toHex()),
                            }),
                        }),
                    ])
                    const rawPacketOptions = packetSentEvents[0]!.args.options
                    expect(rawPacketOptions).toEqualCaseInsensitive(options.toHex())

                    // test decoding
                    const packetOptions = Options.fromOptions(rawPacketOptions)
                    const decodedExecutorLzReceiveOption = packetOptions.decodeExecutorLzReceiveOption()
                    expect(decodedExecutorLzReceiveOption).toEqual({
                        gas: gasLimit,
                        value: nativeDrop,
                    })
                    expect(packetOptions.decodeExecutorNativeDropOption()).toEqual([])
                    expect(packetOptions.decodeExecutorComposeOption()).toEqual([])
                    expect(packetOptions.decodeExecutorOrderedExecutionOption()).toEqual(false)
                }
            ),
            { numRuns: 20 }
        )
    })


it('executorComposeOption', async () => {
        await fc.assert(
            fc.asyncProperty(
                omnicounterIncrementTypeArbitrary,
                composedIndexArbitrary,
                gasLimitArbitrary,
                nativeDropArbitrary,

                // Test the generation and submission of arbitrary COMPOSE Options.  The transaction should succeed, and
                // the options from the transaction receipt logs should match the generated input.
                async (type, index, gasLimit, nativeDrop) => {
                    const options = Options.newOptions()
                        .addExecutorComposeOption(index, gasLimit, nativeDrop)
                        // We also need to add a lzReceive option to avoid Executor_ZeroLzReceiveGasProvided error
                        .addExecutorLzReceiveOption(MIN_GAS_LIMIT)
                    const packetSentEvents = await incrementAndReturnLogs(type, options)
                    expect(packetSentEvents).toEqual([
                        expect.objectContaining({
                            args: expect.objectContaining({
                                options: expect.toEqualCaseInsensitive(options.toHex()),
                            }),
                        }),
                    ])
                    const rawPacketOptions = packetSentEvents[0]!.args.options
                    expect(rawPacketOptions).toEqualCaseInsensitive(options.toHex())

                    // test decoding
                    const packetOptions = Options.fromOptions(rawPacketOptions)
                    const decodedExecutorComposeOptions = packetOptions.decodeExecutorComposeOption()
                    expect(decodedExecutorComposeOptions).toEqual([
                        {
                            index,
                            gas: gasLimit,
                            value: nativeDrop,
                        },
                    ])
                    expect(packetOptions.decodeExecutorLzReceiveOption()).toEqual({
                        gas: MIN_GAS_LIMIT,
                        value: BigInt(0),
                    })
                    expect(packetOptions.decodeExecutorNativeDropOption()).toEqual([])
                    expect(packetOptions.decodeExecutorOrderedExecutionOption()).toEqual(false)
                }
            ),
            { numRuns: 20 }
        )
    })


it('executorLzNativeDrop', async () => {
        const address = await ethSigner.signer.getAddress()
        await fc.assert(
            fc.asyncProperty(
                omnicounterIncrementTypeArbitrary,
                nativeDropArbitrary,

                // Test the generation and submission of arbitrary NATIVE_DROP Options.  The transaction should succeed,
                // and the options from the transaction receipt logs should match the generated input.
                async (type, nativeDrop) => {
                    const options = Options.newOptions()
                        .addExecutorNativeDropOption(nativeDrop, address)
                        // We also need to add a lzReceive option to avoid Executor_ZeroLzReceiveGasProvided error
                        .addExecutorLzReceiveOption(MIN_GAS_LIMIT)
                    const packetSentEvents = await incrementAndReturnLogs(type, options)
                    expect(packetSentEvents).toEqual([
                        expect.objectContaining({
                            args: expect.objectContaining({
                                options: expect.toEqualCaseInsensitive(options.toHex()),
                            }),
                        }),
                    ])
                    const rawPacketOptions = packetSentEvents[0]!.args.options
                    expect(rawPacketOptions).toEqualCaseInsensitive(options.toHex())

                    // test decoding
                    const packetOptions = Options.fromOptions(rawPacketOptions)
                    const decodedExecutorNativeDropOptions = packetOptions.decodeExecutorNativeDropOption()
                    expect(decodedExecutorNativeDropOptions).toEqual([
                        {
                            amount: nativeDrop,
                            receiver: expect.toEqualCaseInsensitive(makeBytes32(address)),
                        },
                    ])
                    expect(packetOptions.decodeExecutorLzReceiveOption()).toEqual({
                        gas: MIN_GAS_LIMIT,
                        value: BigInt(0),
                    })
                    expect(packetOptions.decodeExecutorComposeOption()).toEqual([])
                    expect(packetOptions.decodeExecutorOrderedExecutionOption()).toEqual(false)
                }
            ),
            { numRuns: 20 }
        )
    })


it('executorOrderedExecutionOption', async () => {
        await fc.assert(
            fc.asyncProperty(
                omnicounterIncrementTypeArbitrary,

                // Test the generation and submission of arbitrary ORDERED Options.  The transaction should succeed, and the
                // options from the transaction receipt logs should match the generated input.
                async (type) => {
                    const options = Options.newOptions()
                        .addExecutorOrderedExecutionOption()
                        // We also need to add a lzReceive option to avoid Executor_ZeroLzReceiveGasProvided error
                        .addExecutorLzReceiveOption(MIN_GAS_LIMIT)
                    const packetSentEvents = await incrementAndReturnLogs(type, options)
                    expect(packetSentEvents).toHaveLength(1)
                    const rawPacketOptions = packetSentEvents[0]!.args.options.toLowerCase()
                    expect(rawPacketOptions).toBe(options.toHex().toLowerCase())

                    // test decoding
                    const packetOptions = Options.fromOptions(rawPacketOptions)
                    expect(packetOptions.decodeExecutorOrderedExecutionOption()).toBe(true)
                    expect(packetOptions.decodeExecutorLzReceiveOption()).toEqual({
                        gas: MIN_GAS_LIMIT,
                        value: BigInt(0),
                    })
                    expect(packetOptions.decodeExecutorComposeOption()).toEqual([])
                    expect(packetOptions.decodeExecutorNativeDropOption()).toEqual([])
                }
            ),
            { numRuns: 10 }
        )
    })


it('stacked options', async () => {
        // custom gasLimit arbitrary so "stacked" compose options won't have a gasLimit sum that overflows MAX_UINT_128
        const stackedGasLimitArbitrary: fc.Arbitrary<bigint> = fc.bigInt({
            min: MIN_GAS_LIMIT,
            max: BigInt('0xFFFFFFFFFFFFFFFF'),
        })

        // custom nativeDrop arbitrary so "stacked" compose options won't have a nativeDrop sum that exceeds DEFAULT_MAX_NATIVE_DROP
        const stackedValueArbitrary: fc.Arbitrary<bigint> = fc.bigInt({
            min: DEFAULT_MIN_NATIVE_DROP,
            max: DEFAULT_MAX_NATIVE_DROP / BigInt(4),
        })

        const address = await ethSigner.signer.getAddress()
        await fc.assert(
            fc.asyncProperty(
                omnicounterIncrementTypeArbitrary,
                composedIndexArbitrary,
                stackedGasLimitArbitrary,
                stackedValueArbitrary,

                // Test the generation of multiple Options in a single Packet.  The transaction should succeed.  Options
                // should be decoded to match inputs.  gasLimit and nativeDrop should be summed for Packets that have
                // multiple COMPOSE options for the same index.
                async (type, index, gasLimit, stackedValue) => {
                    const options = Options.newOptions()
                        .addExecutorComposeOption(index, gasLimit, stackedValue)
                        .addExecutorLzReceiveOption(gasLimit, stackedValue)
                        .addExecutorNativeDropOption(stackedValue, address)
                        .addExecutorComposeOption(index, gasLimit, stackedValue) // Repeat executor compose option to make sure values/gasLimits are summed

                    const packetSentEvents = await incrementAndReturnLogs(type, options)
                    expect(packetSentEvents).toEqual([
                        expect.objectContaining({
                            args: expect.objectContaining({
                                options: expect.toEqualCaseInsensitive(options.toHex()),
                            }),
                        }),
                    ])
                    const packetOptions = Options.fromOptions(packetSentEvents[0]!.args.options)

                    // check executorComposeOption
                    const packetComposeOptions = packetOptions.decodeExecutorComposeOption()
                    expect(packetComposeOptions).toEqual([
                        {
                            index,
                            gas: gasLimit * BigInt(2),
                            // compose options with same index are summed (in this specific case, just multiplied by 2)
                            value: stackedValue * BigInt(2),
                        },
                    ])

                    // check executorLzReceiveOption
                    const packetLzReceiveOption = packetOptions.decodeExecutorLzReceiveOption()
                    expect(packetLzReceiveOption).toEqual({
                        gas: gasLimit,
                        value: stackedValue,
                    })

                    // check executorNativeDropOption
                    const packetNativeDropOptions = packetOptions.decodeExecutorNativeDropOption()
                    expect(packetNativeDropOptions).toEqual([
                        {
                            amount: stackedValue,
                            receiver: expect.toEqualCaseInsensitive(makeBytes32(address)),
                        },
                    ])
                }
            )
        )
    })


it('child no arg event', async () => {
        const receipt = await (await child.emitNoArgEvent()).wait()
        expect(parseLogs(receipt, parent)).toMatchSnapshot()
        expect(parseLogsWithName(receipt, parent, 'NoArgEvent')).toEqual([]) // only the child should emit the event
        expect(parseLogsWithName(receipt, child, 'NoArgEvent')).toHaveLength(1)
        expect(parseLogsWithName(receipt, parallel, 'NoArgEvent')).toEqual([]) // parallel logs should be empty

        fc.assert(
            fc.property(fc.string(), (name) => {
                fc.pre(name !== 'NoArgEvent')
                expect(parseLogsWithName(receipt, parent, name)).toEqual([])
            })
        )
    })