// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

// This is the test for the private implementation of animated icons.
// To make the private API accessible from the test we do not import the
// material material_animated_icons library, but instead, this test file is an
// implementation of that library, using some of the parts of the real
// material_animated_icons, this give the test access to the private APIs.
library material_animated_icons;

import 'dart:math' as math show pi;
import 'dart:ui' show lerpDouble, Offset;
import 'dart:ui' as ui show Paint, Path, Canvas;

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

part 'src/material/animated_icons/animated_icons.dart';
part 'src/material/animated_icons/animated_icons_data.dart';

// We have to import all the generated files in the material library to avoid
// analysis errors (as the generated constants are all referenced in the
// animated_icons library).
part 'src/material/animated_icons/data/add_event.g.dart';
part 'src/material/animated_icons/data/arrow_menu.g.dart';
part 'src/material/animated_icons/data/close_menu.g.dart';
part 'src/material/animated_icons/data/ellipsis_search.g.dart';
part 'src/material/animated_icons/data/event_add.g.dart';
part 'src/material/animated_icons/data/home_menu.g.dart';
part 'src/material/animated_icons/data/list_view.g.dart';
part 'src/material/animated_icons/data/menu_arrow.g.dart';
part 'src/material/animated_icons/data/menu_close.g.dart';
part 'src/material/animated_icons/data/menu_home.g.dart';
part 'src/material/animated_icons/data/pause_play.g.dart';
part 'src/material/animated_icons/data/play_pause.g.dart';
part 'src/material/animated_icons/data/search_ellipsis.g.dart';
part 'src/material/animated_icons/data/view_list.g.dart';

class MockCanvas extends Mock implements Canvas {}
class MockPath extends Mock implements Path {}

void main() {
  group('Interpolate points', () {
    test('- single point', () {
      const List<Offset> points = <Offset>[
        Offset(25.0, 1.0),
      ];
      expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
      expect(_interpolate(points, 0.5, Offset.lerp), const Offset(25.0, 1.0));
      expect(_interpolate(points, 1.0, Offset.lerp), const Offset(25.0, 1.0));
    });

    test('- two points', () {
      const List<Offset> points = <Offset>[
        Offset(25.0, 1.0),
        Offset(12.0, 12.0),
      ];
      expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
      expect(_interpolate(points, 0.5, Offset.lerp), const Offset(18.5, 6.5));
      expect(_interpolate(points, 1.0, Offset.lerp), const Offset(12.0, 12.0));
    });

    test('- three points', () {
      const List<Offset> points = <Offset>[
        Offset(25.0, 1.0),
        Offset(12.0, 12.0),
        Offset(23.0, 9.0),
      ];
      expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
      expect(_interpolate(points, 0.25, Offset.lerp), const Offset(18.5, 6.5));
      expect(_interpolate(points, 0.5, Offset.lerp), const Offset(12.0, 12.0));
      expect(_interpolate(points, 0.75, Offset.lerp), const Offset(17.5, 10.5));
      expect(_interpolate(points, 1.0, Offset.lerp), const Offset(23.0, 9.0));
    });
  });

  group('_AnimatedIconPainter', () {
    const Size size = Size(48.0, 48.0);
    late MockPath mockPath;
    late MockCanvas mockCanvas;
    late List<MockPath> generatedPaths;
    late _UiPathFactory pathFactory;

    setUp(() {
      generatedPaths = <MockPath>[];
      mockCanvas = MockCanvas();
      mockPath = MockPath();
      pathFactory = () {
        generatedPaths.add(mockPath);
        return mockPath;
      };
    });

    test('progress 0', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 0.0]),
        MockCall('lineTo', <dynamic>[48.0, 0.0]),
        MockCall('lineTo', <dynamic>[48.0, 10.0]),
        MockCall('lineTo', <dynamic>[0.0, 10.0]),
        MockCall('lineTo', <dynamic>[0.0, 0.0]),
        MockCall('close'),
      ]);
    });

    test('progress 1', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(1.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 38.0]),
        MockCall('lineTo', <dynamic>[48.0, 38.0]),
        MockCall('lineTo', <dynamic>[48.0, 48.0]),
        MockCall('lineTo', <dynamic>[0.0, 48.0]),
        MockCall('lineTo', <dynamic>[0.0, 38.0]),
        MockCall('close'),
      ]);
    });

    test('clamped progress', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(1.5),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 38.0]),
        MockCall('lineTo', <dynamic>[48.0, 38.0]),
        MockCall('lineTo', <dynamic>[48.0, 48.0]),
        MockCall('lineTo', <dynamic>[0.0, 48.0]),
        MockCall('lineTo', <dynamic>[0.0, 38.0]),
        MockCall('close'),
      ]);
    });

    test('scale', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 0.5,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      mockCanvas.verifyCallsInOrder(<MockCall>[
        MockCall('scale', <dynamic>[0.5, 0.5]),
        MockCall.any('drawPath'),
      ]);
    });

    test('mirror', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: true,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      mockCanvas.verifyCallsInOrder(<MockCall>[
        MockCall('scale', <dynamic>[1.0, 1.0]),
        MockCall('rotate', <dynamic>[math.pi]),
        MockCall('translate', <dynamic>[-48.0, -48.0]),
        MockCall.any('drawPath'),
      ]);
    });

    test('interpolated frame', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _movingBar.paths,
        progress: const AlwaysStoppedAnimation<double>(0.5),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 19.0]),
        MockCall('lineTo', <dynamic>[48.0, 19.0]),
        MockCall('lineTo', <dynamic>[48.0, 29.0]),
        MockCall('lineTo', <dynamic>[0.0, 29.0]),
        MockCall('lineTo', <dynamic>[0.0, 19.0]),
        MockCall('close'),
      ]);
    });

    test('curved frame', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(1.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 24.0]),
        MockCall('cubicTo', <dynamic>[16.0, 48.0, 32.0, 48.0, 48.0, 24.0]),
        MockCall('lineTo', <dynamic>[0.0, 24.0]),
        MockCall('close'),
      ]);
    });

    test('interpolated curved frame', () {
      final _AnimatedIconPainter painter = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.25),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );
      painter.paint(mockCanvas, size);
      expect(generatedPaths.length, 1);

      generatedPaths[0].verifyCallsInOrder(<MockCall>[
        MockCall('moveTo', <dynamic>[0.0, 24.0]),
        MockCall('cubicTo', <dynamic>[16.0, 17.0, 32.0, 17.0, 48.0, 24.0]),
        MockCall('lineTo', <dynamic>[0.0, 24.0]),
        MockCall('close', <dynamic>[]),
      ]);
    });

    test('should not repaint same values', () {
      final _AnimatedIconPainter painter1 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      final _AnimatedIconPainter painter2 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      expect(painter1.shouldRepaint(painter2), false);
    });

    test('should repaint on progress change', () {
      final _AnimatedIconPainter painter1 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      final _AnimatedIconPainter painter2 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.1),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      expect(painter1.shouldRepaint(painter2), true);
    });

    test('should repaint on color change', () {
      final _AnimatedIconPainter painter1 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF00FF00),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      final _AnimatedIconPainter painter2 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFFFF0000),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      expect(painter1.shouldRepaint(painter2), true);
    });

    test('should repaint on paths change', () {
      final _AnimatedIconPainter painter1 = _AnimatedIconPainter(
        paths: _bow.paths,
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF0000FF),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      final _AnimatedIconPainter painter2 = _AnimatedIconPainter(
        paths: const <_PathFrames>[],
        progress: const AlwaysStoppedAnimation<double>(0.0),
        color: const Color(0xFF0000FF),
        scale: 1.0,
        shouldMirror: false,
        uiPathFactory: pathFactory,
      );

      expect(painter1.shouldRepaint(painter2), true);
    });
  });
}

// Contains the data from an invocation used for collection of calls and for
// expectations in Mock class.
class MockCall {
  // Creates a mock call with optional positional arguments.
  MockCall(String memberName, [this.positionalArguments, this.acceptAny = false])
      : memberSymbol = Symbol(memberName);
  MockCall.fromSymbol(this.memberSymbol, [this.positionalArguments, this.acceptAny = false]);
  // Creates a mock call expectation that doesn't care about what the arguments were.
  MockCall.any(String memberName)
      : memberSymbol = Symbol(memberName),
        acceptAny = true,
        positionalArguments = null;

  final Symbol memberSymbol;
  String get memberName {
    final RegExp symbolMatch = RegExp(r'Symbol\("(?<name>.*)"\)');
    final RegExpMatch? match = symbolMatch.firstMatch(memberSymbol.toString());
    assert(match != null);
    return match!.namedGroup('name')!;
  }

  final List<dynamic>? positionalArguments;
  final bool acceptAny;

  @override
  String toString() {
    return '$memberName(${positionalArguments?.join(', ') ?? ''})';
  }
}

// A very simplified version of a Mock class.
//
// Only verifies positional arguments, and only can verify calls in order.
class Mock {
  final List<MockCall> _calls = <MockCall>[];

  // Verify that the given calls happened in the order given.
  void verifyCallsInOrder(List<MockCall> expected) {
    int count = 0;
    expect(expected.length, equals(_calls.length),
        reason: 'Incorrect number of calls received. '
            'Expected ${expected.length} and received ${_calls.length}.\n'
            '  Calls Received: $_calls\n'
            '  Calls Expected: $expected');
    for (final MockCall call in _calls) {
      expect(call.memberSymbol, equals(expected[count].memberSymbol),
          reason: 'Unexpected call to ${call.memberName}, expected a call to '
              '${expected[count].memberName} instead.');
      if (call.positionalArguments != null && !expected[count].acceptAny) {
        int countArg = 0;
        for (final dynamic arg in call.positionalArguments!) {
          expect(arg, equals(expected[count].positionalArguments![countArg]),
              reason: 'Failed at call $count. Positional argument $countArg to ${call.memberName} '
                  'not as expected. Expected ${expected[count].positionalArguments![countArg]} '
                  'and received $arg');
          countArg++;
        }
      }
      count++;
    }
  }

  @override
  void noSuchMethod(Invocation invocation) {
    _calls.add(MockCall.fromSymbol(invocation.memberName, invocation.positionalArguments));
  }
}

const _AnimatedIconData _movingBar = _AnimatedIconData(
  Size(48.0, 48.0),
  <_PathFrames>[
    _PathFrames(
      opacities: <double>[1.0, 0.2],
      commands: <_PathCommand>[
        _PathMoveTo(
          <Offset>[
            Offset.zero,
            Offset(0.0, 38.0),
          ],
        ),
        _PathLineTo(
          <Offset>[
            Offset(48.0, 0.0),
            Offset(48.0, 38.0),
          ],
        ),
        _PathLineTo(
          <Offset>[
            Offset(48.0, 10.0),
            Offset(48.0, 48.0),
          ],
        ),
        _PathLineTo(
          <Offset>[
            Offset(0.0, 10.0),
            Offset(0.0, 48.0),
          ],
        ),
        _PathLineTo(
          <Offset>[
            Offset.zero,
            Offset(0.0, 38.0),
          ],
        ),
        _PathClose(),
      ],
    ),
  ],
);

const _AnimatedIconData _bow = _AnimatedIconData(
  Size(48.0, 48.0),
  <_PathFrames>[
    _PathFrames(
      opacities: <double>[1.0, 1.0],
      commands: <_PathCommand>[
        _PathMoveTo(
          <Offset>[
            Offset(0.0, 24.0),
            Offset(0.0, 24.0),
            Offset(0.0, 24.0),
          ],
        ),
        _PathCubicTo(
          <Offset>[
            Offset(16.0, 24.0),
            Offset(16.0, 10.0),
            Offset(16.0, 48.0),
          ],
          <Offset>[
            Offset(32.0, 24.0),
            Offset(32.0, 10.0),
            Offset(32.0, 48.0),
          ],
          <Offset>[
            Offset(48.0, 24.0),
            Offset(48.0, 24.0),
            Offset(48.0, 24.0),
          ],
        ),
        _PathLineTo(
          <Offset>[
            Offset(0.0, 24.0),
            Offset(0.0, 24.0),
            Offset(0.0, 24.0),
          ],
        ),
        _PathClose(),
      ],
    ),
  ],
);
