import { Assert, TestLabel } from '@ephox/bedrock-client';
import { Arr, Fun, Id, Strings } from '@ephox/katamari';

import type { ArrayAssert, StringAssert } from './ApproxStructures';

const missingValuePlaceholder: string = Id.generate('missing');

const dieWith = (message: string): () => never => Fun.die(message);

const assertOnBool = (c: boolean, label: TestLabel, value: any): void => {
  const strValue = value === missingValuePlaceholder ? '{missing}' : value;
  Assert.eq(
    TestLabel.concat(label, () => ', Actual value: ' + JSON.stringify(strValue)),
    true,
    c
  );
};

export type CombinedAssert = StringAssert & ArrayAssert;

const is = (target: string): CombinedAssert => {
  const compare = (actual: string) => target === actual;

  const strAssert = (label: TestLabel, actual: string) => {
    const c = compare(actual);
    assertOnBool(c, TestLabel.concat(label, '\nExpected value: ' + JSON.stringify(target)), actual);
  };

  return {
    show: Fun.constant('is("' + target + '")'),
    strAssert,
    arrAssert: dieWith('"is" is not an array assertion. Perhaps you wanted "has"?')
  };
};

const startsWith = (target: string): CombinedAssert => {
  const compare = (actual: string) => Strings.startsWith(actual, target);

  const strAssert = (label: TestLabel, actual: string) => {
    const c = compare(actual);
    assertOnBool(c, TestLabel.concat(label, '\nExpected value: ' + 'startsWith(' + target + ')'), actual);
  };

  return {
    show: Fun.constant('startsWith("' + target + '")'),
    strAssert,
    arrAssert: dieWith('"startsWith" is not an array assertion. Perhaps you wanted "hasPrefix"?')
  };
};

const contains = (target: string): CombinedAssert => {
  const compare = (actual: string) => Strings.contains(actual, target);

  const strAssert = (label: TestLabel, actual: string) => {
    const c = compare(actual);
    assertOnBool(c, TestLabel.concat(label, '\nExpected value: ' + 'contains(' + target + ')'), actual);
  };

  return {
    show: Fun.constant('contains("' + target + '")'),
    strAssert,
    arrAssert: dieWith('"contains" is not an array assertion. Perhaps you wanted "has"?')
  };
};

const measurement = (amount: number, unit: string, margin: number): CombinedAssert => {
  const strAssert = (label: TestLabel, actual: string) => {
    const optValue = Strings.toFloat(actual);
    return optValue.fold(
      () => {
        // There is no valid number, so fail.
        throw new Error(`"${actual}" was not a valid measurement`);
      },
      (value) => {
        const actualUnit = Strings.removeLeading(actual, `${value}`);
        if (actualUnit !== unit) {
          throw new Error(`"${actual}" did not have the correct unit. Expected: "${unit}", but was: "${actualUnit}"`);
        } else {
          // Compare the value, with an error margin.
          if (Math.abs(amount - value) > margin) {
            throw new Error(`"${actual}" was not within "${margin}${unit}" of the expected value: "${amount}${unit}"`);
          }
        }
      }
    );
  };

  return {
    show: Fun.constant(`measurement("${amount} +/- ${margin}", ${unit}`),
    strAssert,
    arrAssert: dieWith('"measurement" is not an array assertion')
  };
};

const none = (message: string = '[[missing value]]'): CombinedAssert => {
  const compare = (actual: string) => actual === missingValuePlaceholder;

  const strAssert = (label: TestLabel, actual) => {
    const c = compare(actual);
    assertOnBool(c, TestLabel.concat(label, '\nExpected ' + message), actual);
  };

  return {
    show: Fun.constant('none("' + message + '")'),
    strAssert,
    arrAssert: dieWith('"none" is not an array assertion. Perhaps you wanted "not"?')
  };
};

const has = <T>(target: T): CombinedAssert => {
  const compare = (t: T) => t === target;

  const arrAssert = (label: TestLabel, array: T[]) => {
    const matching = Arr.exists(array, compare);
    assertOnBool(matching, TestLabel.concat(label, 'Expected array to contain: ' + target), array);
  };

  return {
    show: Fun.constant('has("' + target + '")'),
    strAssert: dieWith('"has" is not a string assertion. Perhaps you wanted "is"?'),
    arrAssert
  };
};

const hasPrefix = (prefix: string): CombinedAssert => {
  const compare = (t: string) => Strings.startsWith(t, prefix);

  const arrAssert = (label: TestLabel, array: string[]) => {
    const matching = Arr.exists(array, compare);
    assertOnBool(matching, TestLabel.concat(label, 'Expected array to contain something with prefix: ' + prefix), array);
  };

  return {
    show: Fun.constant('hasPrefix("' + prefix + '")'),
    strAssert: dieWith('"hasPrefix" is not a string assertion. Perhaps you wanted "startsWith"?'),
    arrAssert
  };
};

const not = <T>(target: T): CombinedAssert => {
  const compare = (actual: T) => target !== actual;

  const arrAssert = (label: TestLabel, array: T[]) => {
    // For not, all have to pass the comparison
    const matching = Arr.forall(array, compare);
    assertOnBool(matching, TestLabel.concat(label, 'Expected array to not contain: ' + target), array);
  };

  return {
    show: Fun.constant('not("' + target + '")'),
    strAssert: dieWith('"not" is not a string assertion. Perhaps you wanted "none"?'),
    arrAssert
  };
};

const missing: () => string = Fun.constant(missingValuePlaceholder);

export {
  is,
  startsWith,
  contains,
  none,
  measurement,

  has,
  hasPrefix,
  not,

  missing
};
