// These cases test that the body of the function indeed respects the defined type guard.

class A {}
class B extends A {}
class M {}

function no_return(x: unknown): x is number {} // error no return

function return_true(x: unknown): x is number {
    return true; // error x: unknown ~> number
}

function return_true_in_branch(x: unknown): x is number {
  if (0 < 1) {
    return true; // error in this branch x: unknown ~> number
  } else {
    return typeof x === 'number';
  }
}

function negation_on_union(x: number | string | boolean): x is boolean {
  return (typeof x !== 'number' && typeof x !== 'string'); // okay
}

function always_false_error(x: unknown): x is A {
  return false; // error (negation) A ~> empty
}

function return_false_under_condition(x: number | string | boolean): x is boolean {
  if (typeof x === 'number') {
    return false; // okay (in else branch we'll get number|string which includes number)
  } else if (typeof x ==='string') {
    return false; // okay (in else branch we'll get number|string which includes string)
  } else {
    return true; // okay (x is boolean and we never hit the else branch through here)
  }
}

const arrow_okay = (x: unknown): x is number => (
  typeof x === 'number' // okay
);

const arrow_error = (x: unknown): x is number => (
  typeof x === 'number' ||
  typeof x === 'string' // error x: string ~> number
);

class C {
  m(x: unknown): x is number {
    return typeof x === 'string'; // error x: string ~> number and (negation) number ~> empty
  }
}

function null_checking() {
  const tes1 = (x: ?number): x is string => ( // error on string because string ~/> ?number
    x != null // error number ~> string
  );

  const test2 = (x: ?number): x is number => (
    x !== null // error null | undefined ~> string
  );

  const test3 = (x: ?number): x is number => (
    x != null // okay
  );
}

function contextual() {
  declare const arr: Array<?number>;

  arr.filter((x): x is string => ( // error on string because string ~/> ?number
    x != null // error number ~> string
  ));

  arr.filter((x): x is number => (
    x !== null // error null | undefined ~> string
  ));

  arr.filter((x): x is number => (
    x != null // okay
  ));

  type Foo = {type: 'foo', a: string};
  type Bar = {type: 'bar', b: number};
  declare const fooBarArr: Array<Foo | Bar>;

  const mappedArr = fooBarArr.filter((x): x is Foo => (
    x.type === 'foo'
  ));
  mappedArr as Array<Foo>;
}


function instanceof_error_1(x: unknown): x is A {
  return x instanceof B; // error (negation) A ~> empty
}

function instanceof_error_2(x: unknown): x is B {
  return x instanceof A; // error A ~> B
}

function nested_functions() {
  function outer(x: unknown): x is A {
    function inner(x: unknown): x is C {
      return x instanceof C; // okay
    };
    return x instanceof A; // okay
  }
}

// This is okay because NonNullable<X> is considered a subtype of X
function non_maybe_poly_1<X>(x: X): x is NonNullable<X> {
  return x != null;
}

function non_maybe_poly_2<X extends {...}>(x: ?X): x is X { // okay
  return x != null;
}

function is_array(x: unknown): x is ReadonlyArray<unknown> { // okay
  return Array.isArray(x);
}

function is_array_poly_error<A>(x: unknown): x is ReadonlyArray<A> { // error ROArray<mixed> ~> ROArray<A>
  return Array.isArray(x);
}

function pipe_result() {
  declare function isNumberOrString(x: unknown): x is (number | string);
  declare function isString(x: number | string): x is string;
  declare function isBoolean(x: unknown): x is boolean;

  function okay(x: unknown): x is string {
    return isNumberOrString(x) && isString(x); // okay
  }

  function error(x: unknown): x is string {
    return isNumberOrString(x) && isBoolean(x); // error (negation) string ~> empty
  }
}


//
// Writes
//

function error_write_to_type_guard_param_1(x: unknown): x is A {
  x = 1;
  return x instanceof A; // error 'x' is written to
}

function error_write_to_type_guard_param_2(x: unknown): x is B {
  x = new B();
  return x instanceof B; // error 'x' is written to
}

function error_write_to_type_guard_param_multi(x: unknown): x is B {
  if (0 < 1) {
    x = new B();
    return x instanceof B; // error 'x' is written to (1st loc)
  } else {
    x = new B();
    return x instanceof B; // error 'x' is written to (2nd loc)
  }
}

function multi_return_ok(x: unknown): x is A {
  if (0 < 1) {
    return x instanceof A; // okay
  }
  return x instanceof B; // error (negation) A ~> empty
}

function multi_return_one_branch_error(x: unknown): x is B {
  if (0 < 1) {
    return x instanceof A; // error A ~> B
  }
  return x instanceof B; // okay
}

//
// Havoc
//

function havoc_error(x: unknown): x is B { // error 'x' is havoced
  function y() {
    x = 1;
  };
  y();
  return x instanceof B;
}

function havoc_ok_no_call(x: unknown): x is B {
  function y() {
    x = 1;
  };
  return x instanceof B; // okay 'y' is not called
}

function havoc_ok_new_var(x: unknown): x is B {
  function y() {
    let x = 2;
    x = 1;
  };
  y();
  return x instanceof B; // okay local (to 'y') 'x' is written to
}

class Private_property_1 {
  #prop: (value: unknown) => implies value is number;

  test1(value: unknown): implies value is string { // error number ~> string on predicate
    return this.#prop(value); // no prop-missing error
  }

  test2(value: unknown): implies value is number { // okay
    return this.#prop(value); // no prop-missing error
  }
}

class Private_property_2 {
  remote: Private_property_1;
  #prop: (value: unknown) => implies value is number; // required to prevent parse error below

  test(value: unknown, remote: Private_property_1): implies value is string { // error number ~> string on predicate
    return remote.#prop(value); // error prop-missing
  }
}
