// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:test/test.dart';

import 'env.dart';
import 'utils.dart';

void main() {
  var x = 'x';
  var y = 'y';
  var z = 'z';

  group('sealed | ', () {
    // Here, "(_)" means "sealed". A bare name is unsealed.
    //
    //     (A)
    //     / \
    //   (B) (C)
    //   / \   \
    //  D   E   F
    //         / \
    //        G   H
    var env = TestEnvironment();
    var a = env.createClass('A', isSealed: true);
    var b = env.createClass('B', isSealed: true, inherits: [a]);
    var c = env.createClass('C', isSealed: true, inherits: [a]);
    var d = env.createClass('D', inherits: [b]);
    var e = env.createClass('E', inherits: [b]);
    var f = env.createClass('F', inherits: [c]);
    var g = env.createClass('G', inherits: [f]);
    var h = env.createClass('H', inherits: [f]);

    test('exhaustiveness', () {
      // Case matching top type covers all subtypes.
      expectExhaustiveness(env, a, [a]);
      expectExhaustiveness(env, b, [a]);
      expectExhaustiveness(env, d, [a]);

      // Case matching subtype doesn't cover supertype.
      expectExhaustiveness(env, a, [b],
          errors: 'A is not exhaustively matched by B.');
      expectExhaustiveness(env, b, [b]);
      expectExhaustiveness(env, d, [b]);
      expectExhaustiveness(env, e, [b]);

      // Matching subtypes of sealed type is exhaustive.
      expectExhaustiveness(env, a, [b, c]);
      expectExhaustiveness(env, a, [d, e, f]);
      expectExhaustiveness(env, a, [b, f]);
      expectExhaustiveness(env, a, [c, d],
          errors: 'A is not exhaustively matched by C|D.');
      expectExhaustiveness(env, f, [g, h],
          errors: 'F is not exhaustively matched by G|H.');
    });

    test('unreachable case', () {
      // Same type.
      expectExhaustiveness(env, b, [b, b], errors: 'Case #2 B is unreachable.');

      // Previous case is supertype.
      expectExhaustiveness(env, b, [a, b], errors: 'Case #2 B is unreachable.');

      // Previous subtype cases cover sealed supertype.
      expectExhaustiveness(env, a, [b, c, a],
          errors: 'Case #3 A is unreachable.');
      expectExhaustiveness(env, a, [d, e, f, a],
          errors: 'Case #4 A is unreachable.');
      expectExhaustiveness(env, a, [b, f, a],
          errors: 'Case #3 A is unreachable.');
      expectExhaustiveness(env, a, [c, d, a]);

      // Previous subtype cases do not cover unsealed supertype.
      expectExhaustiveness(env, f, [g, h, f]);

      // Guarded case is reachable
      expectExhaustiveness(env, b, [d, e, d],
          caseIsGuarded: [true, false, false]);

      // Guarded case is unreachable
      expectExhaustiveness(env, b, [d, e, d],
          caseIsGuarded: [false, false, true],
          errors: 'Case #3 D is unreachable.');
    });

    test('covered record destructuring |', () {
      var r = env.createRecordType({x: a, y: a, z: a});

      // Wider field is not covered.
      expectExhaustiveness(env, r, [
        ty(r, {x: b}),
        ty(r, {x: a}),
      ]);

      // Narrower field is covered.
      expectExhaustiveness(
          env,
          r,
          [
            ty(r, {x: a}),
            ty(r, {x: b}),
          ],
          errors: 'Case #2 (x: B, y: A, z: A) is unreachable.');
    });

    test('nullable sealed |', () {
      //     (A)
      //     / \
      //    B  (C)
      //       / \
      //      D   E
      var env = TestEnvironment();
      var a = env.createClass('A', isSealed: true);
      var b = env.createClass('B', inherits: [a]);
      var c = env.createClass('C', isSealed: true, inherits: [a]);
      var d = env.createClass('D', inherits: [c]);
      var e = env.createClass('E', inherits: [c]);

      // Must cover null.
      expectExhaustiveness(env, a.nullable, [b, d, e],
          errors: 'A? is not exhaustively matched by B|D|E.');

      // Can cover null with any nullable subtype.
      expectExhaustiveness(env, a.nullable, [b.nullable, c]);
      expectExhaustiveness(env, a.nullable, [b, c.nullable]);
      expectExhaustiveness(env, a.nullable, [b, d.nullable, e]);
      expectExhaustiveness(env, a.nullable, [b, d, e.nullable]);

      // Can cover null with a null space.
      expectExhaustiveness(env, a.nullable, [b, c, StaticType.nullType]);
      expectExhaustiveness(env, a.nullable, [b, d, e, StaticType.nullType]);

      // Nullable covers the non-null.
      expectExhaustiveness(env, a.nullable, [a.nullable, a],
          errors: 'Case #2 A is unreachable.');
      expectExhaustiveness(env, b.nullable, [a.nullable, b],
          errors: 'Case #2 B is unreachable.');

      // Nullable covers null.
      expectExhaustiveness(env, a.nullable, [a.nullable, StaticType.nullType],
          errors: 'Case #2 Null is unreachable.');
      expectExhaustiveness(env, b.nullable, [a.nullable, StaticType.nullType],
          errors: 'Case #2 Null is unreachable.');
    });
  });
}

void expectExhaustiveness(ObjectPropertyLookup objectFieldLookup,
    StaticType valueType, List<Object> cases,
    {List<bool>? caseIsGuarded, String errors = ''}) {
  List<CaseUnreachability> caseUnreachabilities = [];
  var nonExhaustiveness = computeExhaustiveness(objectFieldLookup, valueType,
      caseIsGuarded ?? List.filled(cases.length, false), parseSpaces(cases),
      caseUnreachabilities: caseUnreachabilities);
  expect(
      [
        ...caseUnreachabilities,
        if (nonExhaustiveness != null) nonExhaustiveness
      ].join('\n'),
      errors);
}
