// 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:test/test.dart';

import '../mini_ast.dart';
import '../mini_types.dart';

main() {
  late Harness h;
  late InterfaceTypeName a;
  late InterfaceTypeName b1;
  late InterfaceTypeName b2;

  setUp(() {
    TypeRegistry.init();
    a = TypeRegistry.addInterfaceTypeName('A');
    TypeRegistry.addInterfaceTypeName('B');
    b1 = TypeRegistry.addInterfaceTypeName('B1');
    b2 = TypeRegistry.addInterfaceTypeName('B2');
    TypeRegistry.addInterfaceTypeName('C');
    TypeRegistry.addInterfaceTypeName('C1');
    TypeRegistry.addInterfaceTypeName('C2');
    TypeRegistry.addInterfaceTypeName('E');
    TypeRegistry.addInterfaceTypeName('X');
    h = Harness();
  });

  tearDown(() {
    TypeRegistry.uninit();
  });

  group('Collection elements:', () {
    group('If:', () {
      test('Condition schema', () {
        h.run([
          listLiteral(elementType: 'int', [
            ifElement(
              expr('dynamic').checkSchema('bool'),
              expr('Object'),
            ).checkIR('if(expr(dynamic), celt(expr(Object)), noop)'),
          ]),
        ]);
      });

      test('With else', () {
        h.run([
          listLiteral(elementType: 'int', [
            ifElement(
              expr('bool'),
              expr('Object'),
              expr('Object'),
            ).checkIR('if(expr(bool), celt(expr(Object)), celt(expr(Object)))'),
          ]),
        ]);
      });

      group('Schema:', () {
        test('Element type', () {
          h.run([
            listLiteral(elementType: 'int', [
              ifElement(
                expr('bool'),
                expr('Object').checkSchema('int'),
                expr('Object').checkSchema('int'),
              ).checkIR(
                'if(expr(bool), celt(expr(Object)), celt(expr(Object)))',
              ),
            ]),
          ]);
        });
      });
    });

    group('If-case:', () {
      test('Expression schema', () {
        h.run([
          listLiteral(elementType: 'int', [
            ifCaseElement(
              expr('Object').checkSchema('_'),
              intLiteral(0).pattern,
              intLiteral(1).checkSchema('int'),
            ).checkIR(
              'if(expression: expr(Object), pattern: '
              'const(0, matchedType: Object), guard: true, '
              'ifTrue: celt(1), ifFalse: noop)',
            ),
          ]),
        ]);
      });

      test('With else', () {
        h.run([
          listLiteral(elementType: 'int', [
            ifCaseElement(
              expr('Object'),
              intLiteral(0).pattern,
              intLiteral(1).checkSchema('int'),
              intLiteral(2).checkSchema('int'),
            ).checkIR(
              'if(expression: expr(Object), pattern: '
              'const(0, matchedType: Object), guard: true, '
              'ifTrue: celt(1), ifFalse: celt(2))',
            ),
          ]),
        ]);
      });

      test('With guard', () {
        var x = Var('x');
        h.run([
          listLiteral(elementType: 'int', [
            ifCaseElement(
              expr('Object'),
              x.pattern().when(x.eq(intLiteral(0))),
              intLiteral(1).checkSchema('int'),
            ).checkIR(
              'if(expression: expr(Object), pattern: '
              'varPattern(x, matchedType: Object, staticType: Object), '
              'guard: ==(x, 0), ifTrue: celt(1), ifFalse: noop)',
            ),
          ]),
        ]);
      });

      test('Allows refutable patterns', () {
        var x = Var('x');
        h.run([
          listLiteral(elementType: 'int', [
            ifCaseElement(
              expr('Object'),
              x.pattern(type: 'int'), // has type, refutable
              intLiteral(1).checkSchema('int'),
            ).checkIR(
              'if(expression: expr(Object), pattern: varPattern(x, '
              'matchedType: Object, staticType: int), guard: true, '
              'ifTrue: celt(1), ifFalse: noop)',
            ),
          ]),
        ]);
      });

      group('Guard not assignable to bool', () {
        test('int', () {
          var x = Var('x');
          h.run(
            [
              listLiteral(elementType: 'int', [
                ifCaseElement(
                  expr('Object'),
                  x.pattern().when(expr('int')..errorId = 'GUARD'),
                  intLiteral(0).checkSchema('int'),
                ),
              ]),
            ],
            expectedErrors: {'nonBooleanCondition(node: GUARD)'},
          );
        });

        test('bool', () {
          var x = Var('x');
          h.run([
            listLiteral(elementType: 'int', [
              ifCaseElement(
                expr('Object'),
                x.pattern().when(expr('bool')),
                intLiteral(0).checkSchema('int'),
              ),
            ]),
          ], expectedErrors: {});
        });

        test('dynamic', () {
          var x = Var('x');
          h.run([
            listLiteral(elementType: 'int', [
              ifCaseElement(
                expr('Object'),
                x.pattern().when(expr('dynamic')),
                intLiteral(0).checkSchema('int'),
              ),
            ]),
          ], expectedErrors: {});
        });
      });
    });

    group('Pattern-for-in:', () {
      group('Expression type:', () {
        test('Iterable', () {
          var x = Var('x');
          h.run([
            listLiteral(elementType: 'Object', [
              patternForInElement(
                x.pattern(),
                expr('Iterable<int>'),
                expr('Object'),
              ).checkIR(
                'forEach(expr(Iterable<int>), varPattern(x, '
                'matchedType: int, staticType: int), celt(expr(Object)))',
              ),
            ]),
          ]);
        });
        test('dynamic', () {
          var x = Var('x');
          h.run([
            listLiteral(elementType: 'Object', [
              patternForInElement(
                x.pattern(),
                expr('dynamic'),
                expr('Object'),
              ).checkIR(
                'forEach(expr(dynamic), varPattern(x, '
                'matchedType: dynamic, staticType: dynamic), '
                'celt(expr(Object)))',
              ),
            ]),
          ]);
        });
        test('Object', () {
          var x = Var('x');
          h.run(
            [
              listLiteral(elementType: 'Object', [
                (patternForInElement(
                  x.pattern(),
                  expr('Object')..errorId = 'EXPRESSION',
                  expr('Object'),
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Object), varPattern(x, '
                  'matchedType: error, staticType: error), '
                  'celt(expr(Object)))',
                ),
              ]),
            ],
            expectedErrors: {
              'patternForInExpressionIsNotIterable(node: FOR, '
                  'expression: EXPRESSION, expressionType: Object)',
            },
          );
        });
      });

      group('Refutability:', () {
        test('When a refutable pattern', () {
          var x = Var('x');
          h.run(
            [
              listLiteral(elementType: 'Object', [
                (patternForInElement(
                  x.pattern().nullCheck..errorId = 'PATTERN',
                  expr('Iterable<int?>'),
                  expr('Object'),
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Iterable<int?>), nullCheckPattern('
                  'varPattern(x, matchedType: int, staticType: int), '
                  'matchedType: int?), celt(expr(Object)))',
                ),
              ]),
            ],
            expectedErrors: {
              'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                  'context: FOR)',
            },
          );
        });
        test('When the variable type is not a subtype of the matched type', () {
          var x = Var('x');
          h.run(
            [
              listLiteral(elementType: 'Object', [
                (patternForInElement(
                  x.pattern(type: 'String')..errorId = 'PATTERN',
                  expr('Iterable<int>'),
                  expr('Object'),
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Iterable<int>), varPattern(x, '
                  'matchedType: int, staticType: String), '
                  'celt(expr(Object)))',
                ),
              ]),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: FOR, matchedType: int, requiredType: String)',
            },
          );
        });
      });
    });
  });

  group('Expressions:', () {
    group('cascade:', () {
      group('IR:', () {
        test('not null-aware', () {
          h.run([
            expr('dynamic')
                .cascade([
                  (t) => t.invokeMethod('f', []),
                  (t) => t.invokeMethod('g', []),
                ])
                .checkIR(
                  'let(t0, expr(dynamic), '
                  'let(t1, f(t0), let(t2, g(t0), t0)))',
                ),
          ]);
        });

        test('null-aware', () {
          h.run([
            expr('dynamic')
                .cascade(isNullAware: true, [
                  (t) => t.invokeMethod('f', []),
                  (t) => t.invokeMethod('g', []),
                ])
                .checkIR(
                  'let(t0, expr(dynamic), '
                  'if(==(t0, null), t0, let(t1, f(t0), let(t2, g(t0), t0))))',
                ),
          ]);
        });
      });
    });

    group('integer literal', () {
      test('double type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: true,
          ).checkType('double').checkIR('1.0f').inTypeSchema('double'),
        ]);
      });

      test('int type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: false,
          ).checkType('int').checkIR('1').inTypeSchema('int'),
        ]);
      });

      test('num type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: false,
          ).checkType('int').checkIR('1').inTypeSchema('num'),
        ]);
      });

      test('double? type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: true,
          ).checkType('double').checkIR('1.0f').inTypeSchema('double?'),
        ]);
      });

      test('int? type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: false,
          ).checkType('int').checkIR('1').inTypeSchema('int?'),
        ]);
      });

      test('unknown type schema', () {
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: false,
          ).checkType('int').checkIR('1').inTypeSchema('_'),
        ]);
      });

      test('unrelated type schema', () {
        // Note: an unrelated type schema can arise in the case of assigning to
        // a promoted variable, e.g.:
        //
        //   Object x;
        //   if (x is String) {
        //     x = 1;
        //   }
        h.run([
          intLiteral(
            1,
            expectConversionToDouble: false,
          ).checkType('int').checkIR('1').inTypeSchema('String'),
        ]);
      });
    });

    group('Switch:', () {
      test('IR', () {
        h.run([
          switchExpr(expr('int'), [
            default_.thenExpr(intLiteral(0)),
          ]).checkIR('switchExpr(expr(int), case(default, 0))'),
        ]);
      });

      test('scrutinee expression schema', () {
        h.run([
          switchExpr(expr('int').checkSchema('_'), [
            default_.thenExpr(intLiteral(0)),
          ]).inTypeSchema('num'),
        ]);
      });

      test('body expression schema', () {
        h.run([
          switchExpr(expr('int'), [
            default_.thenExpr(nullLiteral.checkSchema('C?')),
          ]).inTypeSchema('C?'),
        ]);
      });

      test('least upper bound behavior', () {
        h.run([
          switchExpr(expr('int'), [
            intLiteral(0).pattern.thenExpr(expr('int')),
            default_.thenExpr(expr('double')),
          ]).checkType('num'),
        ]);
      });

      test('no cases', () {
        h.run([switchExpr(expr('A'), []).checkType('Never')]);
      });

      test('guard', () {
        var i = Var('i');
        h.run([
          switchExpr(expr('int'), [
            i
                .pattern()
                .when(i.checkType('int').eq(expr('num')).checkSchema('bool'))
                .thenExpr(expr('String')),
          ]).checkIR(
            'switchExpr(expr(int), case(head(varPattern(i, '
            'matchedType: int, staticType: int), ==(i, expr(num)), '
            'variables(i)), expr(String)))',
          ),
        ]);
      });

      group('Guard not assignable to bool', () {
        test('int', () {
          var x = Var('x');
          h.run(
            [
              switchExpr(expr('int'), [
                x
                    .pattern()
                    .when(expr('int')..errorId = 'GUARD')
                    .thenExpr(expr('int')),
              ]),
            ],
            expectedErrors: {'nonBooleanCondition(node: GUARD)'},
          );
        });

        test('bool', () {
          var x = Var('x');
          h.run([
            switchExpr(expr('int'), [
              x.pattern().when(expr('bool')).thenExpr(expr('int')),
            ]),
          ], expectedErrors: {});
        });

        test('dynamic', () {
          var x = Var('x');
          h.run([
            switchExpr(expr('int'), [
              x.pattern().when(expr('dynamic')).thenExpr(expr('int')),
            ]),
          ], expectedErrors: {});
        });
      });

      group('Variables:', () {
        group('logical-or:', () {
          test('consistent', () {
            var x1 = Var('x', identity: 'x1');
            var x2 = Var('x', identity: 'x2');
            PatternVariableJoin('x', expectedComponents: [x1, x2]);
            h.run([
              switchExpr(expr('double'), [
                    x1.pattern().or(x2.pattern()).thenExpr(expr('int')),
                    default_.thenExpr(expr('int')),
                  ])
                  .checkType('int')
                  .checkIR(
                    'switchExpr(expr(double), case(head(logicalOrPattern('
                    'varPattern(x, matchedType: double, staticType: double), '
                    'varPattern(x, matchedType: double, staticType: double), '
                    'matchedType: double), true, '
                    'variables(double x = [x1, x2])), expr(int)), '
                    'case(default, expr(int)))',
                  ),
            ]);
          });
          group('not consistent:', () {
            test('different finality', () {
              var x1 = Var('x', identity: 'x1', isFinal: true);
              var x2 = Var('x', identity: 'x2')..errorId = 'x2';
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run(
                [
                  switchExpr(expr('double'), [
                        x1.pattern().or(x2.pattern()).thenExpr(expr('int')),
                        default_.thenExpr(expr('int')),
                      ])
                      .checkType('int')
                      .checkIR(
                        'switchExpr(expr(double), case(head(logicalOrPattern('
                        'varPattern(x, matchedType: double, staticType: '
                        'double), varPattern(x, matchedType: double, '
                        'staticType: double), matchedType: double), true, '
                        'variables(notConsistent:differentFinalityOrType '
                        'double x = [x1, x2])), expr(int)), case(default, '
                        'expr(int)))',
                      ),
                ],
                expectedErrors: {
                  'inconsistentJoinedPatternVariable(variable: x = [x1, x2], '
                      'component: x2)',
                },
              );
            });
            test('different types', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2')..errorId = 'x2';
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run(
                [
                  switchExpr(expr('double'), [
                        x1
                            .pattern(type: 'double')
                            .or(x2.pattern(type: 'num'))
                            .thenExpr(expr('int')),
                        default_.thenExpr(expr('int')),
                      ])
                      .checkType('int')
                      .checkIR(
                        'switchExpr(expr(double), case(head(logicalOrPattern('
                        'varPattern(x, matchedType: double, staticType: '
                        'double), varPattern(x, matchedType: double, '
                        'staticType: num), matchedType: double), true, '
                        'variables(notConsistent:differentFinalityOrType error '
                        'x = [x1, x2])), expr(int)), case(default, expr(int)))',
                      ),
                ],
                expectedErrors: {
                  'inconsistentJoinedPatternVariable(variable: x = [x1, x2], '
                      'component: x2)',
                },
              );
            });
          });
        });
      });

      group('Inference update 3:', () {
        void setupTypesForLub() {
          // Class hierarchy:
          //    A
          //    /\
          //   /  \
          // B1<T> B2<T>
          // | \  / |
          // |  \/  |
          // |  /\  |
          // | /  \ |
          // C1<T> C2<T>
          h.addSuperInterfaces('A', (_) => [Type('Object')]);
          h.addSuperInterfaces('B1', (_) => [Type('A'), Type('Object')]);
          h.addSuperInterfaces('B2', (_) => [Type('A'), Type('Object')]);
          h.addSuperInterfaces(
            'C1',
            (args) => [
              PrimaryType(b1, args: args),
              PrimaryType(b2, args: args),
              Type('A'),
              Type('Object'),
            ],
          );
          h.addSuperInterfaces(
            'C2',
            (args) => [
              PrimaryType(b1, args: args),
              PrimaryType(b2, args: args),
              Type('A'),
              Type('Object'),
            ],
          );
          h.addLub('C1<Object?>', 'C2<Object?>', 'A');
          h.addLub('C1<int>', 'C2<double>', 'A');
          h.addLub('B2<Object?>', 'C1<Object?>', 'B2<Object?>');
        }

        test("Context used instead of LUB if LUB doesn't satisfy context", () {
          setupTypesForLub();
          h.run([
            switchExpr(expr('int'), [
              intLiteral(0).pattern.thenExpr(expr('C1<Object?>')),
              wildcard().thenExpr(expr('C2<Object?>')),
            ]).checkType('B1<Object?>').inTypeSchema('B1<Object?>'),
          ]);
        });

        test('Context is converted to a type using greatest closure', () {
          setupTypesForLub();
          h.run([
            switchExpr(expr('int'), [
              intLiteral(0).pattern.thenExpr(expr('C1<int>')),
              wildcard().thenExpr(expr('C2<double>')),
            ]).checkType('B1<Object?>').inTypeSchema('B1<_>'),
          ]);
        });

        test(
          "Context not used if one of the branches doesn't satisfy context",
          () {
            setupTypesForLub();
            h.run([
              switchExpr(expr('int'), [
                intLiteral(0).pattern.thenExpr(expr('C1<Object?>')),
                wildcard().thenExpr(expr('B2<Object?>')),
              ]).checkType('B2<Object?>').inTypeSchema('B1<Object?>'),
            ]);
          },
        );

        test("when disabled, LUB always used, even if it doesn't satisfy "
            "context", () {
          setupTypesForLub();
          h.disableInferenceUpdate3();
          h.run([
            switchExpr(expr('int'), [
              intLiteral(0).pattern.thenExpr(expr('C1<Object?>')),
              wildcard().thenExpr(expr('C2<Object?>')),
            ]).checkType('A').inTypeSchema('B1<Object?>'),
          ]);
        });
      });
    });

    group('Map:', () {
      test('downward inference', () {
        h.run([
          mapLiteral(keyType: 'num', valueType: 'Object', [
            mapEntry(
              expr('int').checkSchema('num'),
              expr('int').checkSchema('Object'),
            ),
          ]),
        ]);
      });

      test('upward inference', () {
        h.run([
          mapLiteral(
            keyType: 'int',
            valueType: 'String',
            [],
          ).checkType('Map<int, String>'),
        ]);
      });

      test('IR', () {
        h.run([
          mapLiteral(keyType: 'int', valueType: 'String?', [
            mapEntry(intLiteral(0), nullLiteral),
          ]).checkIR('map(mapEntry(0, null))'),
        ]);
      });
    });

    group('Method invocation:', () {
      test('Simple', () {
        h.run([
          intLiteral(0)
              .checkSchema('_')
              .invokeMethod('toString', [])
              .checkType('String')
              .checkIR('toString(0)'),
        ]);
      });

      test('Null-aware', () {
        var v = Var('v');
        h.run([
          declare(v, type: 'int?'),
          v
              .checkSchema('_')
              .invokeMethod('toString', [], isNullAware: true)
              .checkType('String')
              .parenthesized // Terminates null shorting
              .checkType('String?')
              .checkIR('let(t0, v, if(==(t0, null), null, toString(t0)))'),
        ]);
      });

      group('Null shorting:', () {
        test('Simple', () {
          var v = Var('v');
          h.run([
            declare(v, type: 'int?'),
            v
                .invokeMethod('abs', [], isNullAware: true)
                .invokeMethod('toString', [])
                .checkType('String')
                .parenthesized // Terminates null shorting
                .checkType('String?')
                .checkIR(
                  'let(t0, v, if(==(t0, null), null, toString(abs(t0))))',
                ),
          ]);
        });

        test('Nested', () {
          var v = Var('v');
          h.run([
            declare(v, type: 'int?'),
            v
                .invokeMethod('abs', [], isNullAware: true)
                .invokeMethod('toString', [], isNullAware: true)
                .checkType('String')
                .parenthesized // Terminates null shorting
                .checkType('String?')
                .checkIR(
                  'let(t0, v, if(==(t0, null), null, let(t1, abs(t0), '
                  'if(==(t1, null), null, toString(t1)))))',
                ),
          ]);
        });
      });
    });

    group('Property get:', () {
      test('Simple', () {
        h.run([
          intLiteral(0)
              .checkSchema('_')
              .property('isEven')
              .checkType('bool')
              .checkIR('get_isEven(0)'),
        ]);
      });

      test('Null-aware', () {
        var v = Var('v');
        h.run([
          declare(v, type: 'int?'),
          v
              .checkSchema('_')
              .property('isEven', isNullAware: true)
              .checkType('bool')
              .parenthesized // Terminates null shorting
              .checkType('bool?')
              .checkIR('let(t0, v, if(==(t0, null), null, get_isEven(t0)))'),
        ]);
      });

      group('Null shorting:', () {
        test('Simple', () {
          var v = Var('v');
          h.run([
            declare(v, type: 'int?'),
            v
                .invokeMethod('abs', [], isNullAware: true)
                .property('isEven')
                .checkType('bool')
                .parenthesized // Terminates null shorting
                .checkType('bool?')
                .checkIR(
                  'let(t0, v, if(==(t0, null), null, get_isEven(abs(t0))))',
                ),
          ]);
        });

        test('Nested', () {
          var v = Var('v');
          h.run([
            declare(v, type: 'int?'),
            v
                .invokeMethod('abs', [], isNullAware: true)
                .property('isEven', isNullAware: true)
                .checkType('bool')
                .parenthesized // Terminates null shorting
                .checkType('bool?')
                .checkIR(
                  'let(t0, v, if(==(t0, null), null, let(t1, abs(t0), '
                  'if(==(t1, null), null, get_isEven(t1)))))',
                ),
          ]);
        });
      });
    });
  });

  group('Statements:', () {
    group('If:', () {
      test('Condition schema', () {
        h.run([
          if_(expr('dynamic').checkSchema('bool'), [
            expr('Object'),
          ]).checkIR('if(expr(dynamic), block(stmt(expr(Object))), noop)'),
        ]);
      });

      test('With else', () {
        h.run([
          if_(expr('bool'), [expr('Object')], [expr('String')]).checkIR(
            'if(expr(bool), block(stmt(expr(Object))), '
            'block(stmt(expr(String))))',
          ),
        ]);
      });
    });

    group('If-case:', () {
      test('Type schema', () {
        var x = Var('x');
        h.run([
          ifCase(
            expr('int').checkSchema('_'),
            x.pattern(type: 'num'),
            [],
          ).checkIR(
            'ifCase(expr(int), '
            'varPattern(x, matchedType: int, staticType: num), variables(x), '
            'true, block(), noop)',
          ),
        ]);
      });

      test('With else', () {
        var x = Var('x');
        h.run([
          ifCase(
            expr('num'),
            x.pattern(type: 'int'),
            [expr('Object')],
            [expr('String')],
          ).checkIR(
            'ifCase(expr(num), '
            'varPattern(x, matchedType: num, staticType: int), variables(x), '
            'true, block(stmt(expr(Object))), block(stmt(expr(String))))',
          ),
        ]);
      });

      test('With guard', () {
        var x = Var('x');
        h.run([
          ifCase(
            expr('num'),
            x.pattern(type: 'int').when(x.eq(intLiteral(0))),
            [],
          ).checkIR(
            'ifCase(expr(num), '
            'varPattern(x, matchedType: num, staticType: int), variables(x), '
            '==(x, 0), block(), noop)',
          ),
        ]);
      });

      test('Allows refutable patterns', () {
        var x = Var('x');
        h.run([
          ifCase(
            expr('num').checkSchema('_'),
            x.pattern(type: 'int'),
            [],
          ).checkIR(
            'ifCase(expr(num), '
            'varPattern(x, matchedType: num, staticType: int), variables(x), '
            'true, block(), noop)',
          ),
        ]);
      });

      group('Guard not assignable to bool', () {
        test('int', () {
          var x = Var('x');
          h.run(
            [
              ifCase(
                expr('int'),
                x.pattern().when(expr('int')..errorId = 'GUARD'),
                [],
              ),
            ],
            expectedErrors: {'nonBooleanCondition(node: GUARD)'},
          );
        });

        test('bool', () {
          var x = Var('x');
          h.run([
            ifCase(expr('int'), x.pattern().when(expr('bool')), []),
          ], expectedErrors: {});
        });

        test('dynamic', () {
          var x = Var('x');
          h.run([
            ifCase(expr('int'), x.pattern().when(expr('dynamic')), []),
          ], expectedErrors: {});
        });
      });
    });

    group('Switch:', () {
      test('Empty', () {
        h.run([switch_(expr('int'), [], expectLastCaseTerminates: true)]);
      });

      test('Exhaustive', () {
        h.addExhaustiveness('E', true);
        h.run([
          switch_(expr('E'), [
            expr('E').pattern.then([break_()]),
          ], expectIsExhaustive: true),
        ]);
      });

      test('No default', () {
        h.run([
          switch_(
            expr('int'),
            [
              intLiteral(0).pattern.then([break_()]),
            ],
            expectHasDefault: false,
            expectIsExhaustive: false,
          ),
        ]);
      });

      test('Has default', () {
        h.run([
          switch_(
            expr('int'),
            [
              intLiteral(0).pattern.then([break_()]),
              default_.then([break_()]),
            ],
            expectHasDefault: true,
            expectIsExhaustive: true,
          ),
        ]);
      });

      test('Last case terminates', () {
        h.run([
          switch_(expr('int'), [
            intLiteral(0).pattern.then([expr('int')]),
            intLiteral(1).pattern.then([break_()]),
          ], expectLastCaseTerminates: true),
        ]);
      });

      test("Last case doesn't terminate", () {
        h.run([
          switch_(expr('int'), [
            intLiteral(0).pattern.then([break_()]),
            intLiteral(1).pattern.then([expr('int')]),
          ], expectLastCaseTerminates: false),
        ]);
      });

      test('Scrutinee type', () {
        h.run([switch_(expr('int'), [], expectScrutineeType: 'int')]);
      });

      test('const pattern', () {
        h.run([
          switch_(expr('int').checkSchema('_'), [
            intLiteral(0).pattern.then([break_()]),
          ]).checkIR(
            'switch(expr(int), case(heads(head(const(0, '
            'matchedType: int), true, variables()), variables()), '
            'block(break())))',
          ),
        ]);
      });

      group('var pattern:', () {
        test('untyped', () {
          var x = Var('x');
          h.run([
            switch_(expr('int').checkSchema('_'), [
              x.pattern().then([break_()]),
            ]).checkIR(
              'switch(expr(int), case(heads(head(varPattern(x, '
              'matchedType: int, staticType: int), true, variables(x)), '
              'variables(x)), block(break())))',
            ),
          ]);
        });

        test('typed', () {
          var x = Var('x');
          h.run([
            switch_(expr('int').checkSchema('_'), [
              x.pattern(type: 'num').then([break_()]),
            ]).checkIR(
              'switch(expr(int), case(heads(head(varPattern(x, '
              'matchedType: int, staticType: num), true, variables(x)), '
              'variables(x)), block(break())))',
            ),
          ]);
        });
      });

      test('scrutinee expression schema', () {
        h.run([
          switch_(expr('int').checkSchema('_'), [
            intLiteral(0).pattern.then([break_()]),
          ]),
        ]);
      });

      test('empty final case', () {
        h.run([
          switch_(expr('int'), [
            intLiteral(0).pattern.then([break_()]),
            intLiteral(1).pattern.then([]),
          ]).checkIR(
            'switch(expr(int), case(heads(head(const(0, '
            'matchedType: int), true, variables()), variables()), '
            'block(break())), case(heads(head(const(1, matchedType: int), '
            'true, variables()), variables()), block(synthetic-break())))',
          ),
        ]);
      });

      test('guard', () {
        var i = Var('i');
        h.run([
          switch_(expr('int'), [
            i
                .pattern()
                .when(i.checkType('int').eq(expr('num')).checkSchema('bool'))
                .then([break_()]),
          ]).checkIR(
            'switch(expr(int), case(heads(head(varPattern(i, '
            'matchedType: int, staticType: int), ==(i, expr(num)), '
            'variables(i)), variables(i)), block(break())))',
          ),
        ]);
      });

      group('Variables:', () {
        test('Independent cases', () {
          var x = Var('x');
          var y = Var('y');
          h.run([
            switch_(expr('int'), [
              x.pattern().then([break_()]),
              y.pattern().then([break_()]),
            ]).checkIR(
              'switch(expr(int), case(heads(head(varPattern(x, '
              'matchedType: int, staticType: int), true, variables(x)), '
              'variables(x)), block(break())), case(heads(head(varPattern(y, '
              'matchedType: int, staticType: int), true, variables(y)), '
              'variables(y)), block(break())))',
            ),
          ]);
        });
        group('Shared case scope:', () {
          group('Present in both cases:', () {
            test('With the same type and finality', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                switch_(expr('int'), [
                  switchStatementMember(
                    [x1.pattern(), x2.pattern()],
                    [break_()],
                  ),
                ]).checkIR(
                  'switch(expr(int), case(heads(head(varPattern(x, '
                  'matchedType: int, staticType: int), true, variables(x1)), '
                  'head(varPattern(x, matchedType: int, staticType: int), '
                  'true, variables(x2)), variables(int x = [x1, x2])), '
                  'block(break())))',
                ),
              ]);
            });
            test('With the same type and finality, with logical-or', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              var x3 = Var('x', identity: 'x3');
              var x4 = PatternVariableJoin(
                'x',
                expectedComponents: [x1, x2],
                identity: 'x4',
              );
              PatternVariableJoin('x', expectedComponents: [x4, x3]);
              h.run([
                switch_(expr('int'), [
                  switchStatementMember(
                    [x1.pattern().or(x2.pattern()), x3.pattern()],
                    [break_()],
                  ),
                ]).checkIR(
                  'switch(expr(int), case(heads(head(logicalOrPattern('
                  'varPattern(x, matchedType: int, staticType: int), '
                  'varPattern(x, matchedType: int, staticType: int), '
                  'matchedType: int), true, variables(int x = [x1, x2])), '
                  'head(varPattern(x, matchedType: int, staticType: int), '
                  'true, variables(x3)), variables(int x = '
                  '[int x = [x1, x2], x3])), block(break())))',
                ),
              ]);
            });
            group('With different type:', () {
              test('explicit / explicit', () {
                var x1 = Var('x', identity: 'x1');
                var x2 = Var('x', identity: 'x2');
                PatternVariableJoin('x', expectedComponents: [x1, x2]);
                h.run([
                  switch_(expr('int'), [
                    switchStatementMember(
                      [x1.pattern(type: 'num'), x2.pattern(type: 'int')],
                      [break_()],
                    ),
                  ]).checkIR(
                    'switch(expr(int), case(heads(head(varPattern(x, '
                    'matchedType: int, staticType: num), true, '
                    'variables(x1)), head(varPattern(x, matchedType: int, '
                    'staticType: int), true, variables(x2)), '
                    'variables(notConsistent:differentFinalityOrType error '
                    'x = [x1, x2])), block(break())))',
                  ),
                ]);
              });
              test('explicit / implicit', () {
                var x1 = Var('x', identity: 'x1');
                var x2 = Var('x', identity: 'x2');
                PatternVariableJoin('x', expectedComponents: [x1, x2]);
                h.run([
                  switch_(expr('int'), [
                    switchStatementMember(
                      [x1.pattern(type: 'num'), x2.pattern()],
                      [break_()],
                    ),
                  ]).checkIR(
                    'switch(expr(int), case(heads(head(varPattern(x, '
                    'matchedType: int, staticType: num), true, variables('
                    'x1)), head(varPattern(x, matchedType: int, '
                    'staticType: int), true, variables(x2)), '
                    'variables(notConsistent:differentFinalityOrType error '
                    'x = [x1, x2])), block(break())))',
                  ),
                ]);
              });
              test('implicit / implicit', () {
                var x1 = Var('x', identity: 'x1');
                var x2 = Var('x', identity: 'x2');
                PatternVariableJoin('x', expectedComponents: [x1, x2]);
                h.run([
                  switch_(expr('List<int>'), [
                    switchStatementMember(
                      [
                        x1.pattern(),
                        listPattern([x2.pattern()]),
                      ],
                      [break_()],
                    ),
                  ]).checkIR(
                    'switch(expr(List<int>), case(heads(head(varPattern(x, '
                    'matchedType: List<int>, staticType: List<int>), true, '
                    'variables(x1)), head(listPattern(varPattern(x, '
                    'matchedType: int, staticType: int), matchedType: '
                    'List<int>, requiredType: List<int>), true, '
                    'variables(x2)), variables('
                    'notConsistent:differentFinalityOrType error '
                    'x = [x1, x2])), block(break())))',
                  ),
                ]);
              });
            });
            test('With different finality', () {
              var x1 = Var('x', isFinal: true, identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                switch_(expr('int'), [
                  switchStatementMember(
                    [x1.pattern(), x2.pattern()],
                    [break_()],
                  ),
                ]).checkIR(
                  'switch(expr(int), case(heads(head(varPattern(x, '
                  'matchedType: int, staticType: int), true, variables(x1)), '
                  'head(varPattern(x, matchedType: int, staticType: int), '
                  'true, variables(x2)), variables('
                  'notConsistent:differentFinalityOrType int x = [x1, x2])), '
                  'block(break())))',
                ),
              ]);
            });
          });
          test('case has, case not', () {
            var x1 = Var('x', identity: 'x1');
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run([
              switch_(expr('int'), [
                switchStatementMember(
                  [x1.pattern(), intLiteral(0).pattern],
                  [break_()],
                ),
              ]).checkIR(
                'switch(expr(int), case(heads(head(varPattern(x, '
                'matchedType: int, staticType: int), true, variables(x1)), '
                'head(const(0, matchedType: int), true, variables()), '
                'variables(notConsistent:sharedCaseAbsent int x = [x1])), '
                'block(break())))',
              ),
            ]);
          });
          test('case not, case has', () {
            var x1 = Var('x', identity: 'x1');
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run([
              switch_(expr('int'), [
                switchStatementMember(
                  [intLiteral(0).pattern, x1.pattern()],
                  [break_()],
                ),
              ]).checkIR(
                'switch(expr(int), case(heads(head(const(0, '
                'matchedType: int), true, variables()), head(varPattern(x, '
                'matchedType: int, staticType: int), true, variables(x1)), '
                'variables(notConsistent:sharedCaseAbsent int x = [x1])), '
                'block(break())))',
              ),
            ]);
          });
          test('case has, default', () {
            var x1 = Var('x', identity: 'x1');
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run([
              switch_(expr('int'), [
                switchStatementMember([x1.pattern(), default_], [break_()]),
              ]).checkIR(
                'switch(expr(int), case(heads(head(varPattern(x, '
                'matchedType: int, staticType: int), true, variables(x1)), '
                'default, variables(notConsistent:sharedCaseHasLabel int x '
                '= [x1])), block(break())))',
              ),
            ]);
          });
          test('case has, with label', () {
            var x1 = Var('x', identity: 'x1');
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run([
              switch_(expr('int'), [
                switchStatementMember(
                  [x1.pattern()],
                  [break_()],
                  hasLabels: true,
                ),
              ]).checkIR(
                'switch(expr(int), case(heads(head(varPattern(x, '
                'matchedType: int, staticType: int), true, variables(x1)), '
                'variables(notConsistent:sharedCaseHasLabel int x = '
                '[x1])), block(break())))',
              ),
            ]);
          });
        });
      });

      group('Case completes normally:', () {
        test('Reported when patterns disabled', () {
          h.disablePatterns();
          h.run(
            [
              (switch_(expr('int'), [
                intLiteral(0).pattern.then([expr('int')]),
                default_.then([break_()]),
              ], isLegacyExhaustive: true)..errorId = 'SWITCH'),
            ],
            expectedErrors: {
              'switchCaseCompletesNormally(node: SWITCH, caseIndex: 0)',
            },
          );
        });

        test('Handles cases that share a body', () {
          h.disablePatterns();
          h.run(
            [
              (switch_(expr('int'), [
                switchStatementMember(
                  [
                    intLiteral(0).pattern,
                    intLiteral(1).pattern,
                    intLiteral(2).pattern,
                  ],
                  [expr('int')],
                ),
                default_.then([break_()]),
              ], isLegacyExhaustive: true)..errorId = 'SWITCH'),
            ],
            expectedErrors: {
              'switchCaseCompletesNormally(node: SWITCH, caseIndex: 0)',
            },
          );
        });

        test('Not reported when unreachable', () {
          h.disablePatterns();
          h.run([
            switch_(expr('int'), [
              intLiteral(0).pattern.then([break_()]),
              default_.then([break_()]),
            ], isLegacyExhaustive: true),
          ], expectedErrors: {});
        });

        test('Not reported for final case', () {
          h.disablePatterns();
          h.run([
            switch_(expr('int'), [
              intLiteral(0).pattern.then([expr('int')]),
            ], isLegacyExhaustive: false),
          ], expectedErrors: {});
        });

        test('Not reported when patterns enabled', () {
          // When patterns are enabled, there is an implicit `break` at the end
          // of every switch body.
          h.run([
            switch_(expr('int'), [
              intLiteral(0).pattern.then([expr('int')]),
              default_.then([break_()]),
            ]),
          ], expectedErrors: {});
        });
      });

      group('Case expression type mismatch:', () {
        group('Null safe, patterns disabled:', () {
          test('subtype', () {
            h.disablePatterns();
            h.run([
              switch_(expr('num'), [
                expr('int').pattern.then([break_()]),
              ], isLegacyExhaustive: false),
            ]);
          });

          test('supertype', () {
            h.disablePatterns();
            h.run(
              [
                switch_(expr('int')..errorId = 'SCRUTINEE', [
                  (expr(
                    'num',
                  )..errorId = 'EXPRESSION').pattern.then([break_()]),
                ], isLegacyExhaustive: false),
              ],
              expectedErrors: {
                'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
                    'caseExpression: EXPRESSION, scrutineeType: int, '
                    'caseExpressionType: num)',
              },
            );
          });

          test('unrelated types', () {
            h.disablePatterns();
            h.run(
              [
                switch_(expr('int')..errorId = 'SCRUTINEE', [
                  (expr(
                    'String',
                  )..errorId = 'EXPRESSION').pattern.then([break_()]),
                ], isLegacyExhaustive: false),
              ],
              expectedErrors: {
                'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
                    'caseExpression: EXPRESSION, scrutineeType: int, '
                    'caseExpressionType: String)',
              },
            );
          });

          test('dynamic scrutinee', () {
            h.disablePatterns();
            h.run([
              switch_(expr('dynamic'), [
                expr('int').pattern.then([break_()]),
              ], isLegacyExhaustive: false),
            ]);
          });

          test('dynamic case', () {
            h.disablePatterns();
            h.run(
              [
                switch_(expr('int')..errorId = 'SCRUTINEE', [
                  (expr(
                    'dynamic',
                  )..errorId = 'EXPRESSION').pattern.then([break_()]),
                ], isLegacyExhaustive: false),
              ],
              expectedErrors: {
                'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
                    'caseExpression: EXPRESSION, scrutineeType: int, '
                    'caseExpressionType: dynamic)',
              },
            );
          });
        });

        group('Patterns enabled:', () {
          test('subtype', () {
            h.run([
              switch_(expr('num'), [
                expr('int').pattern.then([break_()]),
              ]),
            ]);
          });

          test('supertype', () {
            h.run([
              switch_(expr('int'), [
                expr('num').pattern.then([break_()]),
              ]),
            ]);
          });

          test('unrelated types', () {
            h.run([
              switch_(expr('int'), [
                expr('String').pattern.then([break_()]),
              ]),
            ]);
          });

          test('dynamic scrutinee', () {
            h.run([
              switch_(expr('dynamic'), [
                expr('int').pattern.then([break_()]),
              ]),
            ]);
          });

          test('dynamic case', () {
            h.run([
              switch_(expr('int'), [
                expr('dynamic').pattern.then([break_()]),
              ]),
            ]);
          });
        });
      });

      group('Pre-merged', () {
        // The CFE merges cases that share a body at parse time, so make sure we
        // we can handle merged cases
        test('Empty', () {
          // During CFE error recovery, there can be an empty case.
          h.run([
            switch_(expr('int'), [
              switchStatementMember([], [break_()]),
            ]).checkIR(
              'switch(expr(int), case(heads(variables()), '
              'block(break())))',
            ),
          ], errorRecoveryOK: true);
        });

        test('Multiple', () {
          h.run([
            switch_(expr('int'), [
              switchStatementMember(
                [intLiteral(0).pattern, intLiteral(1).pattern],
                [break_()],
              ),
            ]).checkIR(
              'switch(expr(int), case(heads(head(const(0, '
              'matchedType: int), true, variables()), head(const(1, '
              'matchedType: int), true, variables()), variables()), '
              'block(break())))',
            ),
          ]);
        });
      });

      group('Guard not assignable to bool', () {
        test('int', () {
          var x = Var('x');
          h.run(
            [
              switch_(expr('int'), [
                x.pattern().when(expr('int')..errorId = 'GUARD').then([
                  break_(),
                ]),
              ]),
            ],
            expectedErrors: {'nonBooleanCondition(node: GUARD)'},
          );
        });

        test('bool', () {
          var x = Var('x');
          h.run([
            switch_(expr('int'), [
              x.pattern().when(expr('bool')).then([break_()]),
            ]),
          ], expectedErrors: {});
        });

        test('dynamic', () {
          var x = Var('x');
          h.run([
            switch_(expr('int'), [
              x.pattern().when(expr('dynamic')).then([break_()]),
            ]),
          ], expectedErrors: {});
        });
      });

      group('requiresExhaustivenessValidation:', () {
        test('When a `default` clause is present', () {
          h.addExhaustiveness('E', true);
          h.run([
            switch_(expr('E'), [
              default_.then([break_()]),
            ], expectRequiresExhaustivenessValidation: false),
          ]);
        });

        test('When the scrutinee is an always-exhaustive type', () {
          h.addExhaustiveness('E', true);
          h.run([
            switch_(expr('E'), [
              expr('E').pattern.then([break_()]),
            ], expectRequiresExhaustivenessValidation: true),
          ]);
        });

        test('When the scrutinee is not an always-exhaustive type', () {
          h.addExhaustiveness('C', false);
          h.run([
            switch_(expr('C'), [
              expr('C').pattern.then([break_()]),
            ], expectRequiresExhaustivenessValidation: false),
          ]);
        });

        test('When pattern support is disabled', () {
          h.disablePatterns();
          h.addExhaustiveness('E', true);
          h.run([
            switch_(
              expr('E'),
              [
                expr('E').pattern.then([break_()]),
              ],
              isLegacyExhaustive: true,
              expectRequiresExhaustivenessValidation: false,
            ),
          ]);
        });
      });
    });

    group('Variable declaration:', () {
      test('initialized, typed', () {
        var x = Var('x');
        h.run([
          declare(
            x,
            type: 'num',
            initializer: expr('int').checkSchema('num'),
          ).checkIR(
            'declare(x, expr(int), initializerType: int, staticType: num)',
          ),
        ]);
      });

      test('initialized, untyped', () {
        var x = Var('x');
        h.run([
          declare(x, initializer: expr('int').checkSchema('_')).checkIR(
            'declare(x, expr(int), initializerType: int, staticType: int)',
          ),
        ]);
      });

      test('uninitialized, typed', () {
        var x = Var('x');
        h.run([declare(x, type: 'int').checkIR('declare(x, staticType: int)')]);
      });

      test('uninitialized, untyped', () {
        var x = Var('x');
        h.run([declare(x).checkIR('declare(x, staticType: dynamic)')]);
      });

      test('promoted initializer', () {
        TypeRegistry.addTypeParameter('T');
        var x = Var('x');
        h.run([
          declare(x, initializer: expr('T&int')).checkIR(
            'declare(x, expr(T&int), initializerType: T&int, staticType: T)',
          ),
        ]);
      });

      test('legal late pattern', () {
        var x = Var('x');
        h.run([
          declare(x, initializer: intLiteral(0), isLate: true).checkIR(
            'declare_late(x, 0, initializerType: int, staticType: int)',
          ),
        ]);
      });

      test('illegal refutable pattern', () {
        h.run(
          [
            (patternVariableDeclaration(
              intLiteral(1).pattern..errorId = 'PATTERN',
              intLiteral(0),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });
    });

    group('Pattern-for-in:', () {
      group('sync', () {
        group('Expression context type schema:', () {
          test('Pattern has type', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(type: 'num'),
                expr('List<int>').checkSchema('Iterable<num>'),
                [],
              ).checkIR(
                'forEach(expr(List<int>), varPattern(x, '
                'matchedType: int, staticType: num), block())',
              ),
            ]);
          });
          test('Pattern does not have type', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(),
                expr('List<int>').checkSchema('Iterable<_>'),
                [],
              ).checkIR(
                'forEach(expr(List<int>), varPattern(x, '
                'matchedType: int, staticType: int), block())',
              ),
            ]);
          });
        });
        group('Expression type:', () {
          test('Iterable', () {
            var x = Var('x');
            h.run([
              patternForIn(x.pattern(), expr('Iterable<int>'), []).checkIR(
                'forEach(expr(Iterable<int>), varPattern(x, '
                'matchedType: int, staticType: int), block())',
              ),
            ]);
          });
          test('dynamic', () {
            var x = Var('x');
            h.run([
              patternForIn(x.pattern(), expr('dynamic'), []).checkIR(
                'forEach(expr(dynamic), varPattern(x, '
                'matchedType: dynamic, staticType: dynamic), block())',
              ),
            ]);
          });
          test('Object', () {
            var x = Var('x');
            h.run(
              [
                (patternForIn(
                  x.pattern(),
                  expr('Object')..errorId = 'EXPRESSION',
                  [],
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Object), varPattern(x, '
                  'matchedType: error, staticType: error), block())',
                ),
              ],
              expectedErrors: {
                'patternForInExpressionIsNotIterable(node: FOR, '
                    'expression: EXPRESSION, expressionType: Object)',
              },
            );
          });
          test('error', () {
            var x = Var('x');
            h.run([
              (patternForIn(x.pattern(), expr('error'), [])).checkIR(
                'forEach(expr(error), varPattern(x, '
                'matchedType: error, staticType: error), block())',
              ),
            ]);
          });
        });
        group('Refutability:', () {
          test('When a refutable pattern', () {
            var x = Var('x');
            h.run(
              [
                (patternForIn(
                  x.pattern().nullCheck..errorId = 'PATTERN',
                  expr('Iterable<int?>'),
                  [],
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Iterable<int?>), nullCheckPattern('
                  'varPattern(x, matchedType: int, staticType: int), '
                  'matchedType: int?), block())',
                ),
              ],
              expectedErrors: {
                'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                    'context: FOR)',
              },
            );
          });
          test(
            'When the variable type is not a subtype of the matched type',
            () {
              var x = Var('x');
              h.run(
                [
                  (patternForIn(
                    x.pattern(type: 'String')..errorId = 'PATTERN',
                    expr('Iterable<int>'),
                    [],
                  )..errorId = 'FOR').checkIR(
                    'forEach(expr(Iterable<int>), varPattern(x, '
                    'matchedType: int, staticType: String), block())',
                  ),
                ],
                expectedErrors: {
                  'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                      'context: FOR, matchedType: int, requiredType: String)',
                },
              );
            },
          );
        });
      });
      group('async', () {
        group('Expression context type schema:', () {
          test('Pattern has type', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(type: 'num'),
                expr('Stream<int>').checkSchema('Stream<num>'),
                [],
                hasAwait: true,
              ).checkIR(
                'forEach(expr(Stream<int>), varPattern(x, '
                'matchedType: int, staticType: num), block())',
              ),
            ]);
          });
          test('Pattern does not have type', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(),
                expr('Stream<int>').checkSchema('Stream<_>'),
                [],
                hasAwait: true,
              ).checkIR(
                'forEach(expr(Stream<int>), varPattern(x, '
                'matchedType: int, staticType: int), block())',
              ),
            ]);
          });
        });
        group('Expression type:', () {
          test('Stream', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(),
                expr('Stream<int>'),
                [],
                hasAwait: true,
              ).checkIR(
                'forEach(expr(Stream<int>), varPattern(x, '
                'matchedType: int, staticType: int), block())',
              ),
            ]);
          });
          test('dynamic', () {
            var x = Var('x');
            h.run([
              patternForIn(
                x.pattern(),
                expr('dynamic'),
                [],
                hasAwait: true,
              ).checkIR(
                'forEach(expr(dynamic), varPattern(x, '
                'matchedType: dynamic, staticType: dynamic), block())',
              ),
            ]);
          });
          test('Object', () {
            var x = Var('x');
            h.run(
              [
                (patternForIn(
                  x.pattern(),
                  expr('Object')..errorId = 'EXPRESSION',
                  [],
                  hasAwait: true,
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Object), varPattern(x, '
                  'matchedType: error, staticType: error), block())',
                ),
              ],
              expectedErrors: {
                'patternForInExpressionIsNotIterable(node: FOR, '
                    'expression: EXPRESSION, expressionType: Object)',
              },
            );
          });
        });
        group('Refutability:', () {
          test('When a refutable pattern', () {
            var x = Var('x');
            h.run(
              [
                (patternForIn(
                  x.pattern().nullCheck..errorId = 'PATTERN',
                  expr('Stream<int?>'),
                  [],
                  hasAwait: true,
                )..errorId = 'FOR').checkIR(
                  'forEach(expr(Stream<int?>), nullCheckPattern('
                  'varPattern(x, matchedType: int, staticType: int), '
                  'matchedType: int?), block())',
                ),
              ],
              expectedErrors: {
                'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                    'context: FOR)',
              },
            );
          });
          test(
            'When the variable type is not a subtype of the matched type',
            () {
              var x = Var('x');
              h.run(
                [
                  (patternForIn(
                    x.pattern(type: 'String')..errorId = 'PATTERN',
                    expr('Stream<int>'),
                    [],
                    hasAwait: true,
                  )..errorId = 'FOR').checkIR(
                    'forEach(expr(Stream<int>), varPattern(x, '
                    'matchedType: int, staticType: String), block())',
                  ),
                ],
                expectedErrors: {
                  'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                      'context: FOR, matchedType: int, requiredType: String)',
                },
              );
            },
          );
        });
      });
    });
  });

  group('Patterns:', () {
    group('Cast:', () {
      test('Type schema', () {
        var x = Var('x');
        h.run([
          patternVariableDeclaration(
            x.pattern().as_('int'),
            expr('num').checkSchema('_'),
          ).checkIR(
            'match(expr(num), castPattern(varPattern(x, '
            'matchedType: int, staticType: int), int, matchedType: num))',
          ),
        ]);
      });

      group('Refutable context:', () {
        test('When matched type is a subtype of required type', () {
          h.run(
            [
              ifCase(
                expr('int'),
                wildcard().as_('num')..errorId = 'PATTERN',
                [],
              ).checkIR(
                'ifCase(expr(int), castPattern(wildcardPattern('
                'matchedType: num), num, matchedType: int), '
                'variables(), true, block(), noop)',
              ),
            ],
            expectedErrors: {
              'matchedTypeIsSubtypeOfRequired(pattern: PATTERN, '
                  'matchedType: int, requiredType: num)',
            },
          );
        });
      });

      group('Refutability:', () {
        test('When matched type is a subtype of variable type', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                x.pattern().as_('num')..errorId = 'PATTERN',
                expr('int'),
              ).checkIR(
                'match(expr(int), '
                'castPattern(varPattern(x, matchedType: num, '
                'staticType: num), num, matchedType: int))',
              ),
            ],
            expectedErrors: {
              'matchedTypeIsSubtypeOfRequired(pattern: PATTERN, '
                  'matchedType: int, requiredType: num)',
            },
          );
        });

        test('When matched type is dynamic', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              x.pattern().as_('num'),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), '
              'castPattern(varPattern(x, matchedType: num, '
              'staticType: num), num, matchedType: dynamic))',
            ),
          ]);
        });

        test('When matched type is not a subtype of variable type', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              x.pattern().as_('num'),
              expr('String'),
            ).checkIR(
              'match(expr(String), '
              'castPattern(varPattern(x, matchedType: num, '
              'staticType: num), num, matchedType: String))',
            ),
          ]);
        });
      });

      group('Fully covered due to extension type erasure:', () {
        test('Cast to representation type', () {
          // If an `as` pattern fully covers the matched value type due to
          // extension type erasure, the "matchedTypeIsSubtypeOfRequired"
          // warning should not be issued.
          h.addSuperInterfaces('E', (_) => [Type('Object?')]);
          h.addExtensionTypeErasure('E', 'int');
          h.run([
            ifCase(
              expr('E'),
              wildcard().as_('int'),
              [checkReachable(true)],
              [checkReachable(false)],
            ),
          ]);
        });

        test('Cast to extension type', () {
          // If an `as` pattern fully covers the matched value type due to
          // extension type erasure, the "matchedTypeIsSubtypeOfRequired"
          // warning should not be issued.
          h.addSuperInterfaces('E', (_) => [Type('Object?')]);
          h.addExtensionTypeErasure('E', 'int');
          h.run([
            ifCase(
              expr('int'),
              wildcard().as_('E'),
              [checkReachable(true)],
              [checkReachable(false)],
            ),
          ]);
        });
      });
    });

    group('Const or literal:', () {
      test('Refutability', () {
        h.run(
          [
            (patternVariableDeclaration(
              intLiteral(1).pattern..errorId = 'PATTERN',
              intLiteral(0),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });
    });

    group('Map:', () {
      group('Type schema:', () {
        test('Explicit type arguments', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              mapPatternWithTypeArguments(
                keyType: 'bool',
                valueType: 'int',
                elements: [mapPatternEntry(expr('int'), x.pattern())],
              ),
              expr('dynamic').checkSchema('Map<bool, int>'),
            ),
          ]);
        });
        group('Implicit element type:', () {
          test('No elements', () {
            h.run(
              [
                patternVariableDeclaration(
                  mapPattern([])..errorId = 'PATTERN',
                  expr('dynamic').checkSchema('Map<_, _>'),
                ),
              ],
              expectedErrors: {'emptyMapPattern(pattern: PATTERN)'},
            );
          });

          test('Variable patterns', () {
            var x = Var('x');
            var y = Var('y');
            h.run([
              patternVariableDeclaration(
                mapPattern([
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int?')),
                  mapPatternEntry(expr('bool'), y.pattern(type: 'num')),
                ]),
                expr('dynamic').checkSchema('Map<_, int>'),
              ),
            ]);
          });
        });
      });

      group('Static type:', () {
        test('Explicit type arguments', () {
          var x = Var('x');
          h.run([
            ifCase(
              expr('dynamic'),
              mapPatternWithTypeArguments(
                keyType: 'bool',
                valueType: 'int',
                elements: [
                  mapPatternEntry(
                    expr('Object').checkSchema('bool'),
                    x.pattern(),
                  ),
                ],
              ),
              [],
            ).checkIR(
              'ifCase(expr(dynamic), mapPattern(mapPatternEntry('
              'expr(Object), varPattern(x, matchedType: int, staticType: '
              'int)), matchedType: dynamic, requiredType: Map<bool, int>), '
              'variables(x), true, block(), noop)',
            ),
          ]);
        });

        test('Matched type is a map', () {
          var x = Var('x');
          h.run([
            ifCase(
              expr('Map<bool, int>'),
              mapPattern([
                mapPatternEntry(
                  expr('Object').checkSchema('bool'),
                  x.pattern(),
                ),
              ]),
              [],
            ).checkIR(
              'ifCase(expr(Map<bool, int>), mapPattern(mapPatternEntry('
              'expr(Object), varPattern(x, matchedType: int, staticType: '
              'int)), matchedType: Map<bool, int>, requiredType: '
              'Map<bool, int>), variables(x), true, block(), noop)',
            ),
          ]);
        });

        test('Matched type is dynamic', () {
          var x = Var('x');
          h.run([
            ifCase(
              expr('dynamic'),
              mapPattern([
                mapPatternEntry(expr('Object').checkSchema('_'), x.pattern()),
              ]),
              [],
            ).checkIR(
              'ifCase(expr(dynamic), mapPattern(mapPatternEntry('
              'expr(Object), varPattern(x, matchedType: dynamic, staticType: '
              'dynamic)), matchedType: dynamic, requiredType: '
              'Map<dynamic, dynamic>), variables(x), true, block(), noop)',
            ),
          ]);
        });

        test('Matched type is error', () {
          var x = Var('x');
          h.run([
            ifCase(
              expr('error'),
              mapPattern([
                mapPatternEntry(expr('Object').checkSchema('_'), x.pattern()),
              ]),
              [],
            ).checkIR(
              'ifCase(expr(error), mapPattern(mapPatternEntry('
              'expr(Object), varPattern(x, matchedType: error, staticType: '
              'error)), matchedType: error, requiredType: '
              'Map<error, error>), variables(x), true, block(), noop)',
            ),
          ]);
        });

        test('Matched type is other', () {
          var x = Var('x');
          h.run([
            ifCase(
              expr('String'),
              mapPattern([
                mapPatternEntry(expr('Object').checkSchema('_'), x.pattern()),
              ]),
              [],
            ).checkIR(
              'ifCase(expr(String), mapPattern(mapPatternEntry('
              'expr(Object), varPattern(x, matchedType: Object?, staticType: '
              'Object?)), matchedType: String, requiredType: '
              'Map<Object?, Object?>), variables(x), true, block(), noop)',
            ),
          ]);
        });
      });

      group('Refutable context:', () {
        test('When matched type is a subtype of required type', () {
          h.run([
            patternVariableDeclaration(
              mapPatternWithTypeArguments(
                keyType: 'Object',
                valueType: 'num',
                elements: [
                  mapPatternEntry(
                    expr('Object').checkSchema('Object'),
                    wildcard(),
                  ),
                ],
              ),
              expr('Map<bool, int>'),
            ).checkIR(
              'match(expr(Map<bool, int>), mapPattern(mapPatternEntry('
              'expr(Object), wildcardPattern(matchedType: num)), '
              'matchedType: Map<bool, int>, '
              'requiredType: Map<Object, num>))',
            ),
          ]);
        });

        test('When matched type is dynamic', () {
          h.run([
            patternVariableDeclaration(
              mapPatternWithTypeArguments(
                keyType: 'Object',
                valueType: 'num',
                elements: [
                  mapPatternEntry(
                    expr('Object').checkSchema('Object'),
                    wildcard(),
                  ),
                ],
              ),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), mapPattern(mapPatternEntry('
              'expr(Object), wildcardPattern(matchedType: num)), '
              'matchedType: dynamic, requiredType: Map<Object, num>))',
            ),
          ]);
        });

        test('When matched type is not a subtype of required type', () {
          h.run(
            [
              patternVariableDeclaration(
                mapPatternWithTypeArguments(
                  keyType: 'bool',
                  valueType: 'int',
                  elements: [
                    mapPatternEntry(
                      expr('Object').checkSchema('bool'),
                      wildcard(),
                    ),
                  ],
                )..errorId = 'PATTERN',
                expr('String'),
              )..errorId = 'CONTEXT',
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: String, '
                  'requiredType: Map<bool, int>)',
            },
          );
        });
      });

      group('Errors:', () {
        test('Rest pattern first', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                mapPattern([
                  restPattern()..errorId = 'REST_ELEMENT',
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int')),
                ])..errorId = 'MAP_PATTERN',
                expr('dynamic'),
              ),
            ],
            expectedErrors: {
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT)',
            },
          );
        });
        test('Rest pattern last', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                mapPattern([
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int')),
                  restPattern()..errorId = 'REST_ELEMENT',
                ])..errorId = 'MAP_PATTERN',
                expr('dynamic'),
              ),
            ],
            expectedErrors: {
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT)',
            },
          );
        });
        test('Two rest elements at the end', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                mapPattern([
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int')),
                  restPattern()..errorId = 'REST_ELEMENT1',
                  restPattern()..errorId = 'REST_ELEMENT2',
                ])..errorId = 'MAP_PATTERN',
                expr('dynamic'),
              ),
            ],
            expectedErrors: {
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT1)',
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT2)',
            },
          );
        });
        test('Two rest elements not at the end', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                mapPattern([
                  restPattern()..errorId = 'REST_ELEMENT1',
                  restPattern()..errorId = 'REST_ELEMENT2',
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int')),
                ])..errorId = 'MAP_PATTERN',
                expr('dynamic'),
              ),
            ],
            expectedErrors: {
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT1)',
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT2)',
            },
          );
        });
        test('Rest pattern with subpattern', () {
          var x = Var('x');
          h.run(
            [
              patternVariableDeclaration(
                mapPattern([
                  mapPatternEntry(expr('bool'), x.pattern(type: 'int')),
                  restPattern(wildcard())..errorId = 'REST_ELEMENT',
                ])..errorId = 'MAP_PATTERN',
                expr('dynamic'),
              ),
            ],
            expectedErrors: {
              'restPatternInMap(node: MAP_PATTERN, element: REST_ELEMENT)',
            },
          );
        });
      });
    });

    group('List:', () {
      group('Type schema:', () {
        test('Explicit element type', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              listPattern([x.pattern()], elementType: 'int'),
              expr('dynamic').checkSchema('List<int>'),
            ),
          ]);
        });

        group('Implicit element type:', () {
          test('No elements', () {
            h.run([
              patternVariableDeclaration(
                listPattern([]),
                expr('dynamic').checkSchema('List<_>'),
              ),
            ]);
          });

          test('Variable patterns', () {
            var x = Var('x');
            var y = Var('y');
            h.run([
              patternVariableDeclaration(
                listPattern([x.pattern(type: 'int?'), y.pattern(type: 'num')]),
                expr('dynamic').checkSchema('List<int>'),
              ),
            ]);
          });

          group('Rest pattern:', () {
            group('With pattern:', () {
              test('Iterable', () {
                var x = Var('x');
                h.run([
                  patternVariableDeclaration(
                    listPattern([
                      restPattern(x.pattern(type: 'Iterable<int>')),
                    ]),
                    expr('List<int>').checkSchema('List<int>'),
                  ),
                ]);
              });
              test('Not Iterable', () {
                var x = Var('x');
                h.run(
                  [
                    patternVariableDeclaration(
                      listPattern([
                        restPattern(
                          x.pattern(type: 'String')..errorId = 'VAR(x)',
                        ),
                      ]),
                      expr('List<int>').checkSchema('List<_>'),
                    )..errorId = 'CONTEXT',
                  ],
                  expectedErrors: {
                    'patternTypeMismatchInIrrefutableContext('
                        'pattern: VAR(x), context: CONTEXT, matchedType: '
                        'List<int>, requiredType: String)',
                  },
                );
              });
            });
            group('Without pattern:', () {
              test('No other elements', () {
                h.run([
                  patternVariableDeclaration(
                    listPattern([restPattern()]),
                    expr('dynamic').checkSchema('List<_>'),
                  ),
                ]);
              });
              test('Has other elements', () {
                var x = Var('x');
                h.run([
                  patternVariableDeclaration(
                    listPattern([x.pattern(type: 'int'), restPattern()]),
                    expr('dynamic').checkSchema('List<int>'),
                  ),
                ]);
              });
            });
          });
        });
      });

      group('Static type:', () {
        test('Explicit type', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              listPattern([x.pattern(type: 'num')], elementType: 'int'),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), '
              'listPattern(varPattern(x, matchedType: int, '
              'staticType: num), '
              'matchedType: dynamic, requiredType: List<int>))',
            ),
          ]);
        });

        test('Matched type is a list', () {
          var x = Var('x');
          var y = Var('y');
          h.run([
            patternVariableDeclaration(
              listPattern([
                x.pattern(expectInferredType: 'int'),
                restPattern(y.pattern(expectInferredType: 'List<int>')),
              ]),
              expr('List<int>'),
            ).checkIR(
              'match(expr(List<int>), listPattern(varPattern(x, '
              'matchedType: int, staticType: int), ...(varPattern(y, '
              'matchedType: List<int>, staticType: List<int>)), '
              'matchedType: List<int>, requiredType: List<int>))',
            ),
          ]);
        });

        test('Matched type is dynamic', () {
          var x = Var('x');
          var y = Var('y');
          h.run([
            patternVariableDeclaration(
              listPattern([
                x.pattern(expectInferredType: 'dynamic'),
                restPattern(y.pattern(expectInferredType: 'List<dynamic>')),
              ]),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), listPattern(varPattern(x, '
              'matchedType: dynamic, staticType: dynamic), ...(varPattern(y, '
              'matchedType: List<dynamic>, staticType: List<dynamic>)), '
              'matchedType: dynamic, requiredType: List<dynamic>))',
            ),
          ]);
        });

        test('Matched type is error', () {
          var x = Var('x');
          var y = Var('y');
          h.run([
            patternVariableDeclaration(
              listPattern([
                x.pattern(expectInferredType: 'error'),
                restPattern(y.pattern(expectInferredType: 'List<error>')),
              ]),
              expr('error'),
            ).checkIR(
              'match(expr(error), listPattern(varPattern(x, '
              'matchedType: error, staticType: error), ...(varPattern(y, '
              'matchedType: List<error>, staticType: List<error>)), '
              'matchedType: error, requiredType: List<error>))',
            ),
          ]);
        });

        test('Matched type is other', () {
          var x = Var('x');
          var y = Var('y');
          h.run([
            ifCase(
              expr('Object'),
              listPattern([
                x.pattern(expectInferredType: 'Object?'),
                restPattern(y.pattern(expectInferredType: 'List<Object?>')),
              ]),
              [],
            ).checkIR(
              'ifCase(expr(Object), listPattern(varPattern(x, '
              'matchedType: Object?, staticType: Object?), ...(varPattern(y, '
              'matchedType: List<Object?>, staticType: List<Object?>)), '
              'matchedType: Object, requiredType: List<Object?>), '
              'variables(x, y), true, block(), noop)',
            ),
          ]);
        });

        group('Rest pattern:', () {
          test('With pattern', () {
            var x = Var('x');
            h.run([
              patternVariableDeclaration(
                listPattern([restPattern(x.pattern())]),
                expr('List<int>'),
              ).checkIR(
                'match(expr(List<int>), listPattern(...(varPattern(x, '
                'matchedType: List<int>, staticType: List<int>)), '
                'matchedType: List<int>, requiredType: List<int>))',
              ),
            ]);
          });
          test('Without pattern', () {
            h.run([
              patternVariableDeclaration(
                listPattern([restPattern()]),
                expr('List<int>'),
              ).checkIR(
                'match(expr(List<int>), listPattern(..., '
                'matchedType: List<int>, requiredType: List<int>))',
              ),
            ]);
          });
        });
      });

      group('Refutability:', () {
        test('When matched type is a subtype of pattern type', () {
          h.run([
            patternVariableDeclaration(
              listPattern([wildcard()], elementType: 'num'),
              expr('List<int>'),
            ).checkIR(
              'match(expr(List<int>), listPattern(wildcardPattern'
              '(matchedType: num), matchedType: List<int>, '
              'requiredType: List<num>))',
            ),
          ]);
        });

        test('When matched type is dynamic', () {
          h.run([
            patternVariableDeclaration(
              listPattern([wildcard()], elementType: 'num'),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), listPattern(wildcardPattern('
              'matchedType: num), matchedType: dynamic, '
              'requiredType: List<num>))',
            ),
          ]);
        });

        test('When matched type is not a subtype of variable type', () {
          h.run(
            [
              (patternVariableDeclaration(
                listPattern([wildcard()], elementType: 'num')
                  ..errorId = 'PATTERN',
                expr('String'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: String, '
                  'requiredType: List<num>)',
            },
          );
        });

        test('Sub-refutability', () {
          h.run(
            [
              (patternVariableDeclaration(
                listPattern([
                  wildcard(type: 'int')..errorId = 'INT',
                  wildcard(type: 'double')..errorId = 'DOUBLE',
                ], elementType: 'num'),
                expr('List<num>'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: INT, '
                  'context: CONTEXT, matchedType: num, requiredType: int)',
              'patternTypeMismatchInIrrefutableContext(pattern: DOUBLE, '
                  'context: CONTEXT, matchedType: num, requiredType: double)',
            },
          );
        });
      });

      group('Rest pattern:', () {
        group('Duplicate:', () {
          test('With pattern', () {
            var x = Var('x');
            var y = Var('y');
            h.run(
              [
                patternVariableDeclaration(
                  listPattern([
                    restPattern(x.pattern())..errorId = 'ORI',
                    restPattern(y.pattern())..errorId = 'DUP',
                  ])..errorId = 'LIST_PATTERN',
                  expr('List<int>'),
                ).checkIR(
                  'match(expr(List<int>), listPattern(...(varPattern(x, '
                  'matchedType: List<int>, staticType: List<int>)), '
                  '...(varPattern(y, matchedType: List<int>, staticType: '
                  'List<int>)), matchedType: List<int>, '
                  'requiredType: List<int>))',
                ),
              ],
              expectedErrors: {
                'duplicateRestPattern(mapOrListPattern: LIST_PATTERN, '
                    'original: ORI, '
                    'duplicate: DUP)',
              },
            );
          });
          test('Without pattern', () {
            h.run(
              [
                patternVariableDeclaration(
                  listPattern([
                    restPattern()..errorId = 'ORI',
                    restPattern()..errorId = 'DUP',
                  ])..errorId = 'LIST_PATTERN',
                  expr('List<int>'),
                ).checkIR(
                  'match(expr(List<int>), listPattern(..., ..., '
                  'matchedType: List<int>, requiredType: List<int>))',
                ),
              ],
              expectedErrors: {
                'duplicateRestPattern(mapOrListPattern: LIST_PATTERN, '
                    'original: ORI, '
                    'duplicate: DUP)',
              },
            );
          });
        });
        test('First', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              listPattern([restPattern(), x.pattern()]),
              expr('List<int>'),
            ).checkIR(
              'match(expr(List<int>), listPattern(..., '
              'varPattern(x, matchedType: int, staticType: int), '
              'matchedType: List<int>, requiredType: List<int>))',
            ),
          ]);
        });
        test('Last', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              listPattern([x.pattern(), restPattern()]),
              expr('List<int>'),
            ).checkIR(
              'match(expr(List<int>), listPattern(varPattern(x, '
              'matchedType: int, staticType: int), ..., '
              'matchedType: List<int>, requiredType: List<int>))',
            ),
          ]);
        });
      });

      test('Match var overlap', () {
        var x1 = Var('x', identity: 'x1')..errorId = 'x1';
        var x2 = Var('x', identity: 'x2')..errorId = 'x2';
        h.run(
          [
            patternVariableDeclaration(
              listPattern([x1.pattern(), x2.pattern()]),
              expr('List<int>'),
            ),
          ],
          expectedErrors: {
            'duplicateVariablePattern(name: x, original: x1, duplicate: x2)',
          },
        );
      });
    });

    group('Logical-and:', () {
      test('Type schema', () {
        h.run(
          [
            patternVariableDeclaration(
              (wildcard(type: 'int?')..errorId = 'WILDCARD1').and(
                wildcard(type: 'double?')..errorId = 'WILDCARD2',
              ),
              nullLiteral.checkSchema('Null'),
            ).checkIR(
              'match(null, logicalAndPattern(wildcardPattern('
              'matchedType: Null), wildcardPattern(matchedType: Null), '
              'matchedType: Null))',
            ),
          ],
          expectedErrors: {
            'unnecessaryWildcardPattern(pattern: WILDCARD1, '
                'kind: logicalAndPatternOperand)',
            'unnecessaryWildcardPattern(pattern: WILDCARD2, '
                'kind: logicalAndPatternOperand)',
          },
        );
      });

      test('Refutability', () {
        h.run(
          [
            (patternVariableDeclaration(
              (wildcard(type: 'int')..errorId = 'LHS').and(
                wildcard(type: 'double')..errorId = 'RHS',
              ),
              expr('num'),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'patternTypeMismatchInIrrefutableContext(pattern: LHS, '
                'context: CONTEXT, matchedType: num, requiredType: int)',
            'patternTypeMismatchInIrrefutableContext(pattern: RHS, '
                'context: CONTEXT, matchedType: int, requiredType: double)',
          },
        );
      });

      test('Duplicate variable pattern', () {
        var x1 = Var('x', identity: 'x1')..errorId = 'x1';
        var x2 = Var('x', identity: 'x2')..errorId = 'x2';
        h.run(
          [
            patternVariableDeclaration(
              x1.pattern().and(x2.pattern()),
              expr('int'),
            ),
          ],
          expectedErrors: {
            'duplicateVariablePattern(name: x, original: x1, duplicate: x2)',
          },
        );
      });
    });

    group('Logical-or:', () {
      test('Type schema', () {
        h.run(
          [
            (patternVariableDeclaration(
              wildcard(type: 'int?').or(wildcard(type: 'double?'))
                ..errorId = 'PATTERN',
              nullLiteral.checkSchema('_'),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });

      test('Refutability', () {
        // Note: even though the logical-or contains refutable sub-patterns, we
        // don't issue errors for them because they would overlap with the error
        // we're issuing for the logical-or pattern as a whole.
        h.run(
          [
            (patternVariableDeclaration(
              wildcard(type: 'int').or(wildcard(type: 'double'))
                ..errorId = 'PATTERN',
              expr('num'),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });

      group('Variables:', () {
        group('Should have same types:', () {
          group('Same:', () {
            test('explicit / explicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                ifCase(
                  expr('Object'),
                  x1.pattern(type: 'int').or(x2.pattern(type: 'int')),
                  [],
                ).checkIR(
                  'ifCase(expr(Object), logicalOrPattern(varPattern(x, '
                  'matchedType: Object, staticType: int), varPattern(x, '
                  'matchedType: Object, staticType: int), '
                  'matchedType: Object), variables(int x = [x1, x2]), '
                  'true, block(), noop)',
                ),
              ]);
            });
            test('explicit / explicit, normalized', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                ifCase(
                  expr('Object'),
                  x1
                      .pattern(type: 'Object')
                      .or(x2.pattern(type: 'FutureOr<Object>')),
                  [],
                ).checkIR(
                  'ifCase(expr(Object), logicalOrPattern(varPattern(x, '
                  'matchedType: Object, staticType: Object), varPattern(x, '
                  'matchedType: Object, staticType: FutureOr<Object>), '
                  'matchedType: Object), variables(Object x = [x1, x2]), '
                  'true, block(), noop)',
                ),
              ]);
            });
            test('explicit / implicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                ifCase(
                  expr('int'),
                  x1.pattern(type: 'int').or(x2.pattern()),
                  [],
                ).checkIR(
                  'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                  'matchedType: int, staticType: int), varPattern(x, '
                  'matchedType: int, staticType: int), matchedType: int), '
                  'variables(int x = [x1, x2]), true, block(), noop)',
                ),
              ]);
            });
            test('implicit / explicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                ifCase(
                  expr('int'),
                  x1.pattern().or(x2.pattern(type: 'int')),
                  [],
                ).checkIR(
                  'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                  'matchedType: int, staticType: int), varPattern(x, '
                  'matchedType: int, staticType: int), matchedType: int), '
                  'variables(int x = [x1, x2]), true, block(), noop)',
                ),
              ]);
            });
            test('implicit / implicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2');
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run([
                ifCase(expr('int'), x1.pattern().or(x2.pattern()), []).checkIR(
                  'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                  'matchedType: int, staticType: int), varPattern(x, '
                  'matchedType: int, staticType: int), matchedType: int), '
                  'variables(int x = [x1, x2]), true, block(), noop)',
                ),
              ]);
            });
          });
          group('Not same:', () {
            test('explicit / explicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2')..errorId = 'x2';
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run(
                [
                  ifCase(
                    expr('Object'),
                    x1.pattern(type: 'int').or(x2.pattern(type: 'num')),
                    [],
                  ).checkIR(
                    'ifCase(expr(Object), logicalOrPattern(varPattern(x, '
                    'matchedType: Object, staticType: int), varPattern(x, '
                    'matchedType: Object, staticType: num), matchedType: '
                    'Object), variables(notConsistent:differentFinalityOrType '
                    'error x = [x1, x2]), true, block(), noop)',
                  ),
                ],
                expectedErrors: {
                  'inconsistentJoinedPatternVariable(variable: x = [x1, x2], '
                      'component: x2)',
                },
              );
            });
            test('explicit / implicit', () {
              var x1 = Var('x', identity: 'x1');
              var x2 = Var('x', identity: 'x2')..errorId = 'x2';
              PatternVariableJoin('x', expectedComponents: [x1, x2]);
              h.run(
                [
                  ifCase(
                    expr('num'),
                    x1.pattern(type: 'int').or(x2.pattern()),
                    [],
                  ).checkIR(
                    'ifCase(expr(num), logicalOrPattern(varPattern(x, '
                    'matchedType: num, staticType: int), varPattern(x, '
                    'matchedType: num, staticType: num), matchedType: num), '
                    'variables(notConsistent:differentFinalityOrType error x = '
                    '[x1, x2]), true, block(), noop)',
                  ),
                ],
                expectedErrors: {
                  'inconsistentJoinedPatternVariable(variable: x = [x1, x2], '
                      'component: x2)',
                },
              );
            });
          });
        });
        test('Should have same finality', () {
          var x1 = Var('x', isFinal: true, identity: 'x1');
          var x2 = Var('x', identity: 'x2')..errorId = 'x2';
          PatternVariableJoin('x', expectedComponents: [x1, x2]);
          h.run(
            [
              ifCase(expr('int'), x1.pattern().or(x2.pattern()), []).checkIR(
                'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                'matchedType: int, staticType: int), varPattern(x, '
                'matchedType: int, staticType: int), matchedType: int), '
                'variables(notConsistent:differentFinalityOrType int x = '
                '[x1, x2]), true, block(), noop)',
              ),
            ],
            expectedErrors: {
              'inconsistentJoinedPatternVariable(variable: x = [x1, x2], '
                  'component: x2)',
            },
          );
        });
        group('Should be present in both branches:', () {
          test('Both have', () {
            var x1 = Var('x', identity: 'x1');
            var x2 = Var('x', identity: 'x2');
            PatternVariableJoin('x', expectedComponents: [x1, x2]);
            h.run([
              ifCase(expr('int'), x1.pattern().or(x2.pattern()), []).checkIR(
                'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                'matchedType: int, staticType: int), varPattern(x, '
                'matchedType: int, staticType: int), matchedType: int), '
                'variables(int x = [x1, x2]), true, block(), noop)',
              ),
            ]);
          });
          test('Left has', () {
            var x1 = Var('x', identity: 'x1')..errorId = 'x1';
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run(
              [
                ifCase(
                  expr('int'),
                  (x1.pattern().or(wildcard()))..errorId = 'PATTERN',
                  [],
                ).checkIR(
                  'ifCase(expr(int), logicalOrPattern(varPattern(x, '
                  'matchedType: int, staticType: int), wildcardPattern('
                  'matchedType: int), matchedType: int), variables('
                  'notConsistent:logicalOr int x = [x1]), true, block(), '
                  'noop)',
                ),
              ],
              expectedErrors: {
                'logicalOrPatternBranchMissingVariable(node: PATTERN, '
                    'hasInLeft: true, name: x, variable: x1)',
              },
            );
          });
          test('Right has', () {
            var x1 = Var('x', identity: 'x1')..errorId = 'x1';
            PatternVariableJoin('x', expectedComponents: [x1]);
            h.run(
              [
                ifCase(
                  expr('int'),
                  (wildcard().or(x1.pattern()))..errorId = 'PATTERN',
                  [],
                ).checkIR(
                  'ifCase(expr(int), logicalOrPattern(wildcardPattern('
                  'matchedType: int), varPattern(x, matchedType: int, '
                  'staticType: int), matchedType: int), variables('
                  'notConsistent:logicalOr int x = [x1]), true, block(), '
                  'noop)',
                ),
              ],
              expectedErrors: {
                'logicalOrPatternBranchMissingVariable(node: PATTERN, '
                    'hasInLeft: false, name: x, variable: x1)',
              },
            );
          });
        });
      });
    });

    group('Null-assert:', () {
      test('Type schema', () {
        var x = Var('x');
        h.run(
          [
            patternVariableDeclaration(
              x.pattern(type: 'int').nullAssert..errorId = 'PATTERN',
              expr('int').checkSchema('int?'),
            ).checkIR(
              'match(expr(int), '
              'nullAssertPattern(varPattern(x, matchedType: int, '
              'staticType: int), matchedType: int))',
            ),
          ],
          expectedErrors: {
            'matchedTypeIsStrictlyNonNullable(pattern: PATTERN, '
                'matchedType: int)',
          },
        );
      });

      group('Refutability:', () {
        test('When matched type is nullable', () {
          h.run([
            patternVariableDeclaration(
              wildcard().nullAssert,
              expr('int?'),
            ).checkIR(
              'match(expr(int?), nullAssertPattern('
              'wildcardPattern(matchedType: int), matchedType: int?))',
            ),
          ]);
        });

        test('When matched type is non-nullable', () {
          h.run(
            [
              patternVariableDeclaration(
                wildcard().nullAssert..errorId = 'PATTERN',
                expr('int'),
              ).checkIR(
                'match(expr(int), nullAssertPattern('
                'wildcardPattern(matchedType: int), matchedType: int))',
              ),
            ],
            expectedErrors: {
              'matchedTypeIsStrictlyNonNullable(pattern: PATTERN, '
                  'matchedType: int)',
            },
          );
        });

        test('When matched type is dynamic', () {
          h.run([
            patternVariableDeclaration(
              wildcard().nullAssert,
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), nullAssertPattern('
              'wildcardPattern(matchedType: dynamic), '
              'matchedType: dynamic))',
            ),
          ]);
        });

        test('Sub-refutability', () {
          h.run(
            [
              (patternVariableDeclaration(
                (wildcard(type: 'int')..errorId = 'INT').nullAssert
                  ..errorId = 'PATTERN',
                expr('num'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'matchedTypeIsStrictlyNonNullable(pattern: PATTERN, '
                  'matchedType: num)',
              'patternTypeMismatchInIrrefutableContext(pattern: INT, '
                  'context: CONTEXT, matchedType: num, requiredType: int)',
            },
          );
        });
      });

      group('Refutable', () {
        test('When matched type is nullable', () {
          h.run([
            ifCase(expr('int?'), wildcard().nullAssert, []).checkIR(
              'ifCase(expr(int?), nullAssertPattern(wildcardPattern('
              'matchedType: int), matchedType: int?), variables(), true, '
              'block(), noop)',
            ),
          ]);
        });
        test('When matched type is non-nullable', () {
          h.run(
            [
              ifCase(
                expr('int'),
                wildcard().nullAssert..errorId = 'PATTERN',
                [],
              ).checkIR(
                'ifCase(expr(int), nullAssertPattern(wildcardPattern('
                'matchedType: int), matchedType: int), variables(), true, '
                'block(), noop)',
              ),
            ],
            expectedErrors: {
              'matchedTypeIsStrictlyNonNullable(pattern: PATTERN, '
                  'matchedType: int)',
            },
          );
        });
      });
    });

    group('Null-check:', () {
      test('Type schema', () {
        var x = Var('x');
        h.run(
          [
            (patternVariableDeclaration(
              x.pattern(type: 'int').nullCheck..errorId = 'PATTERN',
              expr('int').checkSchema('_'),
            )..errorId = 'CONTEXT'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });

      group('Refutability:', () {
        test('When matched type is nullable', () {
          h.run(
            [
              (patternVariableDeclaration(
                wildcard().nullCheck..errorId = 'PATTERN',
                expr('int?'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT)',
            },
          );
        });

        test('When matched type is non-nullable', () {
          h.run(
            [
              (patternVariableDeclaration(
                wildcard().nullCheck..errorId = 'PATTERN',
                expr('int'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT)',
            },
          );
        });

        test('When matched type is dynamic', () {
          h.run(
            [
              (patternVariableDeclaration(
                wildcard().nullCheck..errorId = 'PATTERN',
                expr('dynamic'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT)',
            },
          );
        });

        test('Sub-refutability', () {
          h.run(
            [
              (patternVariableDeclaration(
                wildcard(type: 'int').nullCheck..errorId = 'PATTERN',
                expr('num'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT)',
            },
          );
        });
      });

      group('Refutable', () {
        test('When matched type is nullable', () {
          h.run([
            ifCase(expr('int?'), wildcard().nullCheck, []).checkIR(
              'ifCase(expr(int?), nullCheckPattern(wildcardPattern('
              'matchedType: int), matchedType: int?), variables(), true, '
              'block(), noop)',
            ),
          ]);
        });
        test('When matched type is non-nullable', () {
          h.run(
            [
              ifCase(
                expr('int'),
                wildcard().nullCheck..errorId = 'PATTERN',
                [],
              ).checkIR(
                'ifCase(expr(int), nullCheckPattern(wildcardPattern('
                'matchedType: int), matchedType: int), variables(), true, '
                'block(), noop)',
              ),
            ],
            expectedErrors: {
              'matchedTypeIsStrictlyNonNullable(pattern: PATTERN, '
                  'matchedType: int)',
            },
          );
        });
      });
    });

    group('Object:', () {
      group('Refutable:', () {
        test('inferred', () {
          h.addDownwardInfer(name: 'B', context: 'A<int>', result: 'B<int>');
          h.addMember('B<int>', 'foo', 'int');
          h.addSuperInterfaces(
            'B',
            (args) => [PrimaryType(a, args: args), Type('Object')],
          );
          h.addSuperInterfaces('A', (_) => [Type('Object')]);
          h.run([
            ifCase(
              expr('A<int>').checkSchema('_'),
              objectPattern(
                requiredType: 'B',
                fields: [Var('foo').pattern().recordField('foo')],
              ),
              [],
            ).checkIR(
              'ifCase(expr(A<int>), objectPattern(varPattern(foo, '
              'matchedType: int, staticType: int), matchedType: A<int>, '
              'requiredType: B<int>), variables(foo), true, block(), noop)',
            ),
          ]);
        });

        test('dynamic type', () {
          h.run([
            ifCase(
              expr('int').checkSchema('_'),
              objectPattern(
                requiredType: 'dynamic',
                fields: [Var('foo').pattern().recordField('foo')],
              ),
              [],
            ).checkIR(
              'ifCase(expr(int), objectPattern(varPattern(foo, '
              'matchedType: dynamic, staticType: dynamic), matchedType: int, '
              'requiredType: dynamic), variables(foo), true, block(), noop)',
            ),
          ]);
        });

        test('error type', () {
          h.run([
            ifCase(
              expr('int').checkSchema('_'),
              objectPattern(
                requiredType: 'error',
                fields: [Var('foo').pattern().recordField('foo')],
              ),
              [],
            ).checkIR(
              'ifCase(expr(int), objectPattern(varPattern(foo, '
              'matchedType: error, staticType: error), matchedType: int, '
              'requiredType: error), variables(foo), true, block(), noop)',
            ),
          ]);
        });

        test('Never type', () {
          h.run([
            ifCase(
              expr('int').checkSchema('_'),
              objectPattern(
                requiredType: 'Never',
                fields: [Var('foo').pattern().recordField('foo')],
              ),
              [],
            ).checkIR(
              'ifCase(expr(int), objectPattern(varPattern(foo, '
              'matchedType: Never, staticType: Never), matchedType: int, '
              'requiredType: Never), variables(foo), true, block(), noop)',
            ),
          ]);
        });

        test('duplicate field name', () {
          h.addMember('A<int>', 'foo', 'int');
          h.run(
            [
              ifCase(
                expr('A<int>'),
                objectPattern(
                  requiredType: 'A<int>',
                  fields: [
                    Var('a').pattern().recordField('foo')..errorId = 'ORIGINAL',
                    Var('b').pattern().recordField('foo')
                      ..errorId = 'DUPLICATE',
                  ],
                )..errorId = 'PATTERN',
                [],
              ),
            ],
            expectedErrors: {
              'duplicateRecordPatternField('
                  'objectOrRecordPattern: PATTERN, '
                  'name: foo, original: ORIGINAL, '
                  'duplicate: DUPLICATE)',
            },
          );
        });
      });

      group('Irrefutable:', () {
        test('assignable', () {
          h.addMember('num', 'foo', 'bool');
          h.run([
            patternVariableDeclaration(
              objectPattern(
                requiredType: 'num',
                fields: [Var('foo').pattern().recordField('foo')],
              ),
              expr('int').checkSchema('num'),
            ).checkIR(
              'match(expr(int), objectPattern(varPattern(foo, '
              'matchedType: bool, staticType: bool), '
              'matchedType: int, requiredType: num))',
            ),
          ]);
        });

        test('not assignable', () {
          h.addMember('int', 'foo', 'bool');
          h.run(
            [
              (patternVariableDeclaration(
                objectPattern(
                  requiredType: 'int',
                  fields: [Var('foo').pattern().recordField('foo')],
                )..errorId = 'PATTERN',
                expr('num').checkSchema('int'),
              )..errorId = 'CONTEXT').checkIR(
                'match(expr(num), objectPattern(varPattern(foo, '
                'matchedType: bool, staticType: bool), '
                'matchedType: num, requiredType: int))',
              ),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: num, requiredType: int)',
            },
          );
        });
      });
    });

    group('Pattern assignment:', () {
      group('Static type:', () {
        test('Matched type is int', () {
          var x = Var('x');
          h.run([
            declare(x, type: 'int'),
            x.pattern().assign(expr('int')).checkType('int'),
          ]);
        });
        test('Matched type is error', () {
          var x = Var('x');
          h.run([
            declare(x, type: 'int'),
            x.pattern().assign(expr('error')).checkType('error'),
          ]);
        });
      });

      test('RHS schema', () {
        var x = Var('x');
        h.run([
          declare(x, type: 'num'),
          x
              .pattern()
              .assign(expr('int').checkSchema('num'))
              .inTypeSchema('Object'),
        ]);
      });

      test('Duplicate assignment to same variable', () {
        var x = Var('x')..errorId = 'x';
        h.run(
          [
            declare(x, type: 'num'),
            recordPattern([
              (x.pattern()..errorId = 'x1').recordField(),
              (x.pattern()..errorId = 'x2').recordField(),
            ]).assign(expr('(int, int)')),
          ],
          expectedErrors: {
            'duplicateAssignmentPatternVariable(variable: x, original: x1, '
                'duplicate: x2)',
          },
        );
      });

      group('Refutability:', () {
        test('When matched type is a subtype of variable type', () {
          var x = Var('x');
          h.run([
            declare(x, type: 'num'),
            x
                .pattern()
                .assign(expr('int'))
                .checkIR('patternAssignment(expr(int), assignedVarPattern(x))'),
          ]);
        });

        test('When matched type is dynamic', () {
          var x = Var('x');
          h.run([
            declare(x, type: 'num'),
            x
                .pattern()
                .assign(expr('dynamic'))
                .checkIR(
                  'patternAssignment(expr(dynamic), assignedVarPattern(x))',
                ),
          ]);
        });

        test('When matched type is not a subtype of variable type', () {
          var x = Var('x');
          h.run(
            [
              declare(x, type: 'num'),
              ((x.pattern()..errorId = 'PATTERN').assign(expr('String'))
                ..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: String, requiredType: num)',
            },
          );
        });
      });
    });

    group('Record:', () {
      group('Positional:', () {
        group('Match dynamic:', () {
          test('refutable', () {
            h.run([
              ifCase(
                expr('dynamic').checkSchema('_'),
                recordPattern([
                  Var('a').pattern(type: 'int').recordField(),
                  Var('b').pattern().recordField(),
                ]),
                [],
              ).checkIR(
                'ifCase(expr(dynamic), recordPattern(varPattern(a, '
                'matchedType: dynamic, staticType: int), varPattern(b, '
                'matchedType: dynamic, staticType: dynamic), matchedType: '
                'dynamic, requiredType: (Object?, Object?)), '
                'variables(a, b), true, block(), noop)',
              ),
            ]);
          });
        });
        group('Match error:', () {
          test('refutable', () {
            h.run([
              ifCase(
                expr('error').checkSchema('_'),
                recordPattern([
                  Var('a').pattern(type: 'int').recordField(),
                  Var('b').pattern().recordField(),
                ]),
                [],
              ).checkIR(
                'ifCase(expr(error), recordPattern(varPattern(a, '
                'matchedType: error, staticType: int), varPattern(b, '
                'matchedType: error, staticType: error), matchedType: '
                'error, requiredType: (Object?, Object?)), '
                'variables(a, b), true, block(), noop)',
              ),
            ]);
          });
        });
        group('Match record type:', () {
          group('Same shape:', () {
            test('irrefutable', () {
              h.run([
                patternVariableDeclaration(
                  recordPattern([
                    Var('a').pattern(type: 'int').recordField(),
                    Var('b').pattern().recordField(),
                  ]),
                  expr('(int, String)').checkSchema('(int, _)'),
                ).checkIR(
                  'match(expr((int, String)), recordPattern(varPattern(a, '
                  'matchedType: int, staticType: int), varPattern(b, '
                  'matchedType: String, staticType: String), '
                  'matchedType: (int, String), '
                  'requiredType: (Object?, Object?)))',
                ),
              ]);
            });
          });
          group('Different shape:', () {
            test('irrefutable', () {
              h.run(
                [
                  (patternVariableDeclaration(
                    recordPattern([
                      (Var(
                        'a',
                      ).pattern(type: 'int')..errorId = 'VAR(a)').recordField(),
                      Var('b').pattern().recordField(),
                    ])..errorId = 'PATTERN',
                    expr('(int,)').checkSchema('(int, _)'),
                  )..errorId = 'CONTEXT').checkIR(
                    'match(expr((int,)), recordPattern(varPattern(a, '
                    'matchedType: Object?, staticType: int), '
                    'varPattern(b, matchedType: Object?, staticType: '
                    'Object?), matchedType: (int,), requiredType: '
                    '(Object?, Object?)))',
                  ),
                ],
                expectedErrors: {
                  'patternTypeMismatchInIrrefutableContext(pattern: VAR(a), '
                      'context: CONTEXT, matchedType: Object?, '
                      'requiredType: int)',
                  'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                      'context: CONTEXT, matchedType: (int,), '
                      'requiredType: (Object?, Object?))',
                },
              );
            });
            group('Refutable:', () {
              test('too few', () {
                h.run([
                  ifCase(
                    expr('(int,)').checkSchema('_'),
                    recordPattern([
                      Var('a').pattern().recordField(),
                      Var('b').pattern().recordField(),
                    ]),
                    [],
                  ).checkIR(
                    'ifCase(expr((int,)), recordPattern(varPattern(a, '
                    'matchedType: Object?, staticType: Object?), '
                    'varPattern(b, matchedType: Object?, staticType: '
                    'Object?), matchedType: (int,), requiredType: '
                    '(Object?, Object?)), variables(a, b), true, '
                    'block(), noop)',
                  ),
                ]);
              });
              test('too many', () {
                h.run([
                  ifCase(
                    expr('(int, String)').checkSchema('_'),
                    recordPattern([Var('a').pattern().recordField()]),
                    [],
                  ).checkIR(
                    'ifCase(expr((int, String)), '
                    'recordPattern(varPattern(a, matchedType: Object?, '
                    'staticType: Object?), matchedType: (int, String), '
                    'requiredType: (Object?,)), variables(a), true, '
                    'block(), noop)',
                  ),
                ]);
              });
            });
          });
        });
        group('Match other type:', () {
          test('refutable', () {
            h.addSuperInterfaces('X', (_) => [Type('Object')]);
            h.run([
              ifCase(
                expr('X').checkSchema('_'),
                recordPattern([
                  Var('a').pattern(type: 'int').recordField(),
                  Var('b').pattern().recordField(),
                ]),
                [],
              ).checkIR(
                'ifCase(expr(X), recordPattern(varPattern(a, '
                'matchedType: Object?, staticType: int), varPattern(b, '
                'matchedType: Object?, staticType: Object?), matchedType: X, '
                'requiredType: (Object?, Object?)), variables(a, b), '
                'true, block(), noop)',
              ),
            ]);
          });
        });
      });
      group('Named:', () {
        group('Match dynamic:', () {
          test('refutable', () {
            h.run([
              ifCase(
                expr('dynamic').checkSchema('_'),
                recordPattern([
                  Var('a').pattern(type: 'int').recordField('a'),
                  Var('b').pattern().recordField('b'),
                ]),
                [],
              ).checkIR(
                'ifCase(expr(dynamic), recordPattern(varPattern(a, '
                'matchedType: dynamic, staticType: int), varPattern(b, '
                'matchedType: dynamic, staticType: dynamic), matchedType: '
                'dynamic, requiredType: ({Object? a, Object? b})), '
                'variables(a, b), true, block(), noop)',
              ),
            ]);
          });
        });
        group('Match record type:', () {
          group('Same shape:', () {
            test('irrefutable', () {
              h.run([
                patternVariableDeclaration(
                  recordPattern([
                    Var('a').pattern(type: 'int').recordField('a'),
                    Var('b').pattern().recordField('b'),
                  ]),
                  expr('({int a, String b})').checkSchema('({int a, _ b})'),
                ).checkIR(
                  'match(expr(({int a, String b})), '
                  'recordPattern(varPattern(a, matchedType: int, '
                  'staticType: int), varPattern(b, matchedType: String, '
                  'staticType: String), matchedType: ({int a, String b}), '
                  'requiredType: ({Object? a, Object? b})))',
                ),
              ]);
            });
          });
          group('Different shape:', () {
            test('irrefutable', () {
              h.run(
                [
                  (patternVariableDeclaration(
                    recordPattern([
                      (Var('a').pattern(
                        type: 'int',
                      )..errorId = 'VAR(a)').recordField('a'),
                      Var('b').pattern().recordField('b'),
                    ])..errorId = 'PATTERN',
                    expr('({int a})').checkSchema('({int a, _ b})'),
                  )..errorId = 'CONTEXT').checkIR(
                    'match(expr(({int a})), '
                    'recordPattern(varPattern(a, matchedType: Object?, '
                    'staticType: int), varPattern(b, matchedType: Object?, '
                    'staticType: Object?), matchedType: ({int a}), '
                    'requiredType: ({Object? a, Object? b})))',
                  ),
                ],
                expectedErrors: {
                  'patternTypeMismatchInIrrefutableContext(pattern: VAR(a), '
                      'context: CONTEXT, matchedType: Object?, '
                      'requiredType: int)',
                  'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                      'context: CONTEXT, matchedType: ({int a}), '
                      'requiredType: ({Object? a, Object? b}))',
                },
              );
            });
            group('Refutable:', () {
              test('too few', () {
                h.run([
                  ifCase(
                    expr('({int a})').checkSchema('_'),
                    recordPattern([
                      Var('a').pattern().recordField('a'),
                      Var('b').pattern().recordField('b'),
                    ]),
                    [],
                  ).checkIR(
                    'ifCase(expr(({int a})), recordPattern('
                    'varPattern(a, matchedType: Object?, staticType: '
                    'Object?), varPattern(b, matchedType: Object?, '
                    'staticType: Object?), matchedType: ({int a}), '
                    'requiredType: ({Object? a, Object? b})), '
                    'variables(a, b), true, block(), noop)',
                  ),
                ]);
              });
              test('too many', () {
                h.run([
                  ifCase(
                    expr('({int a, String b})').checkSchema('_'),
                    recordPattern([Var('a').pattern().recordField('a')]),
                    [],
                  ).checkIR(
                    'ifCase(expr(({int a, String b})), '
                    'recordPattern(varPattern(a, matchedType: Object?, '
                    'staticType: Object?), matchedType: ({int a, String b}), '
                    'requiredType: ({Object? a})), variables(a), true, '
                    'block(), noop)',
                  ),
                ]);
              });
            });
          });
        });
        group('Match other type:', () {
          test('refutable', () {
            h.addSuperInterfaces('X', (_) => [Type('Object')]);
            h.run([
              ifCase(
                expr('X').checkSchema('_'),
                recordPattern([
                  Var('a').pattern(type: 'int').recordField('a'),
                  Var('b').pattern().recordField('b'),
                ]),
                [],
              ).checkIR(
                'ifCase(expr(X), recordPattern(varPattern(a, '
                'matchedType: Object?, staticType: int), varPattern(b, '
                'matchedType: Object?, staticType: Object?), matchedType: X, '
                'requiredType: ({Object? a, Object? b})), variables(a, b), '
                'true, block(), noop)',
              ),
            ]);
          });
        });
        test('duplicate field name', () {
          h.run(
            [
              ifCase(
                expr('({int a})'),
                recordPattern([
                  Var('a').pattern().recordField('a')..errorId = 'ORIGINAL',
                  Var('b').pattern().recordField('a')..errorId = 'DUPLICATE',
                ])..errorId = 'PATTERN',
                [],
              ),
            ],
            expectedErrors: {
              'duplicateRecordPatternField('
                  'objectOrRecordPattern: PATTERN, '
                  'name: a, original: ORIGINAL, '
                  'duplicate: DUPLICATE)',
            },
          );
        });
      });
    });

    group('Relational:', () {
      test('Refutability', () {
        h.run(
          [
            (patternVariableDeclaration(
              relationalPattern('>', intLiteral(0).checkSchema('num'))
                ..errorId = 'PATTERN',
              intLiteral(1).checkSchema('_'),
            )..errorId = 'CONTEXT').checkIR('match(1, >(0, matchedType: int))'),
          ],
          expectedErrors: {
            'refutablePatternInIrrefutableContext(pattern: PATTERN, '
                'context: CONTEXT)',
          },
        );
      });
      test('no operator', () {
        h.addMember('C', '>', null);
        h.run([
          ifCase(
            expr('C').checkSchema('_'),
            relationalPattern('>', intLiteral(0).checkSchema('_')),
            [],
          ).checkIR(
            'ifCase(expr(C), >(0, matchedType: C), '
            'variables(), true, block(), noop)',
          ),
        ]);
      });
      group('Has operator:', () {
        test('int >=', () {
          h.run([
            ifCase(
              expr('int').checkSchema('_'),
              relationalPattern('>=', intLiteral(0).checkSchema('num')),
              [],
            ).checkIR(
              'ifCase(expr(int), >=(0, matchedType: '
              'int), variables(), true, block(), noop)',
            ),
          ]);
        });
        test('Object == nullable', () {
          h.run([
            ifCase(
              expr('Object').checkSchema('_'),
              relationalPattern('==', expr('int?').checkSchema('Object?')),
              [],
            ).checkIR(
              'ifCase(expr(Object), ==(expr(int?), '
              'matchedType: Object), variables(), true, block(), noop)',
            ),
          ]);
        });
        test('Object != nullable', () {
          h.run([
            ifCase(
              expr('Object').checkSchema('_'),
              relationalPattern('!=', expr('int?').checkSchema('Object?')),
              [],
            ).checkIR(
              'ifCase(expr(Object), !=(expr(int?), '
              'matchedType: Object), variables(), true, block(), noop)',
            ),
          ]);
        });

        group('argument type not assignable:', () {
          test('basic', () {
            h.run(
              [
                ifCase(
                  expr('int').checkSchema('_'),
                  relationalPattern('>', expr('String'))..errorId = 'PATTERN',
                  [],
                ).checkIR(
                  'ifCase(expr(int), >(expr(String), '
                  'matchedType: int), variables(), true, block(), noop)',
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: String, parameterType: num)',
              },
            );
          });

          test('> nullable', () {
            h.run(
              [
                ifCase(
                  expr('int'),
                  relationalPattern('>', expr('int?'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: int?, parameterType: num)',
              },
            );
          });

          test('< nullable', () {
            h.run(
              [
                ifCase(
                  expr('int'),
                  relationalPattern('<', expr('int?'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: int?, parameterType: num)',
              },
            );
          });

          test('>= nullable', () {
            h.run(
              [
                ifCase(
                  expr('int'),
                  relationalPattern('>=', expr('int?'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: int?, parameterType: num)',
              },
            );
          });

          test('<= nullable', () {
            h.run(
              [
                ifCase(
                  expr('int'),
                  relationalPattern('<=', expr('int?'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: int?, parameterType: num)',
              },
            );
          });

          test('extension type to representation', () {
            h.addSuperInterfaces('E', (_) => [Type('Object?')]);
            h.addExtensionTypeErasure('E', 'int');
            h.addMember('C', '>', 'bool Function(int)');
            h.run(
              [
                ifCase(
                  expr('C'),
                  relationalPattern('>', expr('E'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: E, parameterType: int)',
              },
            );
          });

          test('representation to extension type', () {
            h.addSuperInterfaces('E', (_) => [Type('Object?')]);
            h.addExtensionTypeErasure('E', 'int');
            h.addMember('C', '>', 'bool Function(E)');
            h.run(
              [
                ifCase(
                  expr('C'),
                  relationalPattern('>', expr('int'))..errorId = 'PATTERN',
                  [],
                ),
              ],
              expectedErrors: {
                'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
                    'operandType: int, parameterType: E)',
              },
            );
          });
        });

        group('argument type assignable:', () {
          test('== nullable', () {
            h.run([
              ifCase(expr('int'), relationalPattern('==', expr('int?')), []),
            ]);
          });

          test('!= nullable', () {
            h.run([
              ifCase(expr('int'), relationalPattern('!=', expr('int?')), []),
            ]);
          });
        });

        test('return type is not assignable to bool', () {
          h.addMember('A', '>', 'int Function(Object)');
          h.run(
            [
              ifCase(
                expr('A').checkSchema('_'),
                relationalPattern(
                  '>',
                  expr('String').checkSchema('Object'),
                  errorId: 'PATTERN',
                ),
                [],
              ).checkIR(
                'ifCase(expr(A), >(expr(String), '
                'matchedType: A), variables(), true, block(), noop)',
              ),
            ],
            expectedErrors: {
              'relationalPatternOperatorReturnTypeNotAssignableToBool('
                  'pattern: PATTERN, returnType: int)',
            },
          );
        });
      });
    });

    group('Variable:', () {
      group('Refutability:', () {
        test('When matched type is a subtype of variable type', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              x.pattern(type: 'num'),
              expr('int'),
            ).checkIR(
              'match(expr(int), '
              'varPattern(x, matchedType: int, staticType: num))',
            ),
          ]);
        });

        test('When matched type is dynamic', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              x.pattern(type: 'num'),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), '
              'varPattern(x, matchedType: dynamic, staticType: num))',
            ),
          ]);
        });

        test('When matched type is error', () {
          var x = Var('x');
          h.run([
            patternVariableDeclaration(
              x.pattern(type: 'num'),
              expr('error'),
            ).checkIR(
              'match(expr(error), '
              'varPattern(x, matchedType: error, staticType: num))',
            ),
          ]);
        });

        test('When matched type is not a subtype of variable type', () {
          var x = Var('x');
          h.run(
            [
              (patternVariableDeclaration(
                x.pattern(type: 'num')..errorId = 'PATTERN',
                expr('String'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: String, requiredType: num)',
            },
          );
        });
      });
    });

    group('Wildcard:', () {
      test('Untyped', () {
        h.run([
          ifCase(expr('int'), wildcard(), []).checkIR(
            'ifCase(expr(int), wildcardPattern(matchedType: int), '
            'variables(), true, block(), noop)',
          ),
        ]);
      });

      test('Typed', () {
        h.run([
          ifCase(expr('num'), wildcard(type: 'int'), []).checkIR(
            'ifCase(expr(num), wildcardPattern(matchedType: num), '
            'variables(), true, block(), noop)',
          ),
        ]);
      });

      group('Refutability:', () {
        test('When matched type is a subtype of variable type', () {
          h.run([
            patternVariableDeclaration(
              wildcard(type: 'num'),
              expr('int'),
            ).checkIR('match(expr(int), wildcardPattern(matchedType: int))'),
          ]);
        });

        test('When matched type is dynamic', () {
          h.run([
            patternVariableDeclaration(
              wildcard(type: 'num'),
              expr('dynamic'),
            ).checkIR(
              'match(expr(dynamic), wildcardPattern('
              'matchedType: dynamic))',
            ),
          ]);
        });

        test('When matched type is not a subtype of variable type', () {
          h.run(
            [
              (patternVariableDeclaration(
                wildcard(type: 'num')..errorId = 'PATTERN',
                expr('String'),
              )..errorId = 'CONTEXT'),
            ],
            expectedErrors: {
              'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
                  'context: CONTEXT, matchedType: String, requiredType: num)',
            },
          );
        });
      });
    });
  });
}
