import {
  assert,
  assertIsArray,
  assertIsBigInt,
  assertIsBoolean,
  assertIsNull,
  assertIsNumber,
  assertIsObject,
  assertIsString,
  assertIsSymbol,
  assertIsUndefined,
  assertIsNotUndefined,
  getAssertionMessage,
  getType,
  isArray,
  isBigInt,
  isBoolean,
  isNull,
  isNumber,
  isObject,
  isString,
  isSymbol,
  isUndefined,
  isNotUndefined,
  setAssertionMessage
} from "../src/index";

const testValues = [
  "",
  "foo",
  "123",
  0,
  Number.MIN_VALUE,
  Number.MAX_VALUE,
  NaN,
  BigInt(0),
  BigInt(Number.MAX_VALUE),
  BigInt(Number.MIN_VALUE - 1),
  true,
  false,
  [],
  [0, 1, 2, 3],
  [null],
  [undefined],
  {},
  { foo: "bar", baz: 123 },
  Symbol(),
  Symbol(123),
  null,
  undefined,
  () => 123
];
const stringList = testValues.filter(value => getType(value) === "string");
const numberList = testValues.filter(value => getType(value) === "number");
const bigintList = testValues.filter(value => getType(value) === "bigint");
const booleanList = testValues.filter(value => getType(value) === "boolean");
const arrayList = testValues.filter(value => Array.isArray(value));
const objectList = testValues.filter(value => getType(value) === "object");
const symbolList = testValues.filter(value => getType(value) === "symbol");
const nullList = testValues.filter(value => getType(value) === "null");
const undefinedList = testValues.filter(
  value => getType(value) === "undefined"
);

/**
 * Helper to check that `assertion` throws an error
 *
 * @param assertion the assertion which should throw
 */
function shouldThrow(assertion: (...args: any) => void) {
  let error: Error | undefined;
  try {
    assertion();
  } catch (err) {
    error = err;
  }
  expect(error).toBeDefined();
}

/**
 * Helper to check that `assertion` does not throw an error
 *
 * @param assertion the assertion which should pass
 */
function shouldNotThrow(assertion: (...args: any) => void) {
  let error: Error | undefined;
  try {
    assertion();
  } catch (err) {
    error = err;
  }
  expect(error).toBeUndefined();
}

describe("isString", () => {
  test("strings pass", () => {
    for (const str of stringList) {
      expect(isString(str)).toBe(true);
    }
  });

  test("non-strings fail", () => {
    for (const nonStr of testValues.filter(v => getType(v) !== "string")) {
      expect(isString(nonStr)).toBe(false);
    }
  });
});

describe("isNumber", () => {
  test("numbers pass", () => {
    for (const num of numberList) {
      expect(isNumber(num)).toBe(true);
    }
  });

  test("non-numbers fail", () => {
    for (const nonNum of testValues.filter(v => getType(v) !== "number")) {
      expect(isNumber(nonNum)).toBe(false);
    }
  });
});

describe("isBigInt", () => {
  test("bigint pass", () => {
    for (const bigint of bigintList) {
      expect(isBigInt(bigint)).toBe(true);
    }
  });

  test("non-bigint fail", () => {
    for (const nonBigInt of testValues.filter(v => getType(v) !== "bigint")) {
      expect(isBigInt(nonBigInt)).toBe(false);
    }
  });
});

describe("isBoolean", () => {
  test("booleans pass", () => {
    for (const bool of booleanList) {
      expect(isBoolean(bool)).toBe(true);
    }
  });

  test("non-booleans fail", () => {
    for (const nonBoolean of testValues.filter(v => getType(v) !== "boolean")) {
      expect(isBoolean(nonBoolean)).toBe(false);
    }
  });
});

describe("isArray", () => {
  test("arrays pass", () => {
    for (const arr of arrayList) {
      expect(isArray(arr)).toBe(true);
    }
  });

  test("non-array fail", () => {
    for (const nonArr of testValues.filter(v => !Array.isArray(v))) {
      expect(isArray(nonArr)).toBe(false);
    }
  });
});

describe("isObject", () => {
  test("objects pass", () => {
    for (const obj of objectList) {
      expect(isObject(obj)).toBe(true);
    }
  });

  test("non-objects fail", () => {
    for (const nonObj of testValues.filter(v => getType(v) !== "object")) {
      expect(isObject(nonObj)).toBe(false);
    }
  });
});

describe("isSymbol", () => {
  test("symbols pass", () => {
    for (const sym of symbolList) {
      expect(isSymbol(sym)).toBe(true);
    }
  });

  test("non-symbols fail", () => {
    for (const nonSym of testValues.filter(v => getType(v) !== "symbol")) {
      expect(isSymbol(nonSym)).toBe(false);
    }
  });
});

describe("isNull", () => {
  test("nulls pass", () => {
    for (const n of nullList) {
      expect(isNull(n)).toBe(true);
    }
  });

  test("non-symbols fail", () => {
    for (const nonNull of testValues.filter(v => getType(v) !== "null")) {
      expect(isNull(nonNull)).toBe(false);
    }
  });
});

describe("isUndefined", () => {
  test("undefined pass", () => {
    for (const undef of undefinedList) {
      expect(isUndefined(undef)).toBe(true);
    }
  });

  test("defined fail", () => {
    for (const defined of testValues.filter(v => getType(v) !== "undefined")) {
      expect(isUndefined(defined)).toBe(false);
    }
  });
});


describe("isNotUndefined", () => {
  test("defined pass", () => {
    for (const defined of testValues.filter(v => getType(v) !== "undefined")) {
      expect(isNotUndefined(defined)).toBe(true);
    }
  });

  test("undefined fail", () => {
    for (const undef of undefinedList) {
      expect(isNotUndefined(undef)).toBe(false);
    }
  });
});

describe("assert", () => {
  test("passing assertions to not throw", () => {
    shouldNotThrow(() => assert(typeof 123 === "number", "should not throw"));
  });

  test("non-passing assertions throw", () => {
    shouldThrow(() => assert(typeof 123 === "string", "should throw"));
  });
});

describe("assertIsArray", () => {
  test("array does not throw", () => {
    for (const arr of arrayList) {
      shouldNotThrow(() => assertIsArray(arr));
    }
  });

  for (const nonArray of testValues.filter(v => !Array.isArray(v))) {
    test(`${getType(nonArray)} throws`, () => {
      shouldThrow(() => assertIsArray(nonArray));
    });
  }
});

describe("assertIsBoolean", () => {
  test("boolean does not throw", () => {
    for (const bool of booleanList) {
      shouldNotThrow(() => assertIsBoolean(bool));
    }
  });

  for (const nonBool of testValues.filter(v => getType(v) !== "boolean")) {
    test(`${getType(nonBool)} throws`, () => {
      shouldThrow(() => assertIsBoolean(nonBool));
    });
  }
});

describe("assertIsNull", () => {
  test("null does not throw", () => {
    for (const nullIsh of nullList) {
      shouldNotThrow(() => assertIsNull(nullIsh));
    }
  });

  for (const nonNull of testValues.filter(v => getType(v) !== "null")) {
    test(`${getType(nonNull)} throws`, () => {
      shouldThrow(() => assertIsNull(nonNull));
    });
  }
});

describe("assertIsNumber", () => {
  test("number does not throw", () => {
    for (const num of numberList) {
      shouldNotThrow(() => assertIsNumber(num));
    }
  });

  for (const nonNum of testValues.filter(v => getType(v) !== "number")) {
    test(`${getType(nonNum)} throws`, () => {
      shouldThrow(() => assertIsNumber(nonNum));
    });
  }
});

describe("assertIsBigInt", () => {
  test("bigint does not throw", () => {
    for (const bigint of bigintList) {
      shouldNotThrow(() => assertIsBigInt(bigint));
    }
  });

  for (const nonBigInt of testValues.filter(v => getType(v) !== "bigint")) {
    test(`${getType(nonBigInt)} throws`, () => {
      shouldThrow(() => assertIsBigInt(nonBigInt));
    });
  }
});

describe("assertIsString", () => {
  test("string does not throw", () => {
    for (const str of stringList) {
      shouldNotThrow(() => assertIsString(str));
    }
  });

  for (const nonStr of testValues.filter(v => getType(v) !== "string")) {
    test(`${getType(nonStr)} throws`, () => {
      shouldThrow(() => assertIsString(nonStr));
    });
  }
});

describe("assertIsUndefined", () => {
  test("undefined does not throw", () => {
    for (const undef of undefinedList) {
      shouldNotThrow(() => assertIsUndefined(undef));
    }
  });

  for (const nonUndef of testValues.filter(v => getType(v) !== "undefined")) {
    test(`${getType(nonUndef)} throws`, () => {
      shouldThrow(() => assertIsUndefined(nonUndef));
    });
  }
});

describe("assertIsObject", () => {
  test("objects do not throw", () => {
    for (const obj of objectList) {
      shouldNotThrow(() => assertIsObject(obj));
    }
  });

  for (const nonObj of testValues.filter(v => getType(v) !== "object")) {
    test(`${getType(nonObj)} throws`, () => {
      shouldThrow(() => assertIsObject(nonObj));
    });
  }
});

describe("assertIsSymbol", () => {
  test("symbols do not throw", () => {
    for (const sym of symbolList) {
      shouldNotThrow(() => assertIsSymbol(sym));
    }
  });

  for (const nonSym of testValues.filter(v => getType(v) !== "symbol")) {
    test(`${getType(nonSym)} throws`, () => {
      shouldThrow(() => assertIsSymbol(nonSym));
    });
  }
});

describe("setAssertionMessage", () => {
  test("it is the default when not modified and variable name not provided", () => {
    let error: Error | undefined;
    try {
      assertIsNumber("not a number");
    } catch (err) {
      error = err;
    }
    expect(error).toBeDefined();
    expect(error!.message).toBe(
      "expected value of type 'number', 'string' provided"
    );
  });

  test("it is the default when not modified and variable name provided", () => {
    let error: Error | undefined;
    try {
      assertIsNumber("not a number", "foobar");
    } catch (err) {
      error = err;
    }
    expect(error).toBeDefined();
    expect(error!.message).toBe(
      "foobar must be of type 'number', 'string' provided"
    );
  });

  test("it is the custom message when set", () => {
    setAssertionMessage(
      (value, type, variableName) =>
        `I am a custom message: ${value} | ${type} | ${variableName}`
    );
    let error: Error | undefined;
    try {
      assertIsNumber("not a number", "foobar");
    } catch (err) {
      error = err;
    }
    expect(error).toBeDefined();
    expect(error!.message).toBe(
      "I am a custom message: not a number | number | foobar"
    );
  });
});

describe("getAssertionMessage", () => {
  const defaultAssertionMessage = getAssertionMessage();
  beforeEach(() => {
    setAssertionMessage(defaultAssertionMessage);
  });

  test("get default, set a new one, set the default back", () => {
    const defaultAssertionMessage = getAssertionMessage();
    const someMessage = "I'm a custom message";
    setAssertionMessage((someValue, someType) => someMessage);

    let error: Error | undefined;
    try {
      assertIsNumber("not a number", "foobar");
    } catch (err) {
      error = err;
    }
    expect(error).toBeDefined();
    expect(error!.message).toBe(someMessage);

    setAssertionMessage(defaultAssertionMessage);
    try {
      assertIsNumber("not a number", "foobar");
    } catch (err) {
      error = err;
    }
    expect(error).toBeDefined();
    expect(error!.message).toBe(
      "foobar must be of type 'number', 'string' provided"
    );
  });
});
