// 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.

// @dart = 2.8

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

class TestCustomPainter extends CustomPainter {
  TestCustomPainter({ this.log, this.name });

  final List<String> log;
  final String name;

  @override
  void paint(Canvas canvas, Size size) {
    log.add(name);
  }

  @override
  bool shouldRepaint(TestCustomPainter oldPainter) => true;
}

class TestCustomPainterWithCustomSemanticsBuilder extends TestCustomPainter {
  TestCustomPainterWithCustomSemanticsBuilder() : super(log: <String>[]);

  @override
  SemanticsBuilderCallback get semanticsBuilder => (Size size) {
    const Key key = Key('0');
    const Rect rect = Rect.fromLTRB(0, 0, 0, 0);
    const SemanticsProperties semanticsProperties = SemanticsProperties();
    return <CustomPainterSemantics>[
      const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties),
      const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties),
    ];
  };
}

class MockCanvas extends Mock implements Canvas {}

class MockPaintingContext extends Mock implements PaintingContext {}

void main() {
  testWidgets('Control test for custom painting', (WidgetTester tester) async {
    final List<String> log = <String>[];
    await tester.pumpWidget(CustomPaint(
      painter: TestCustomPainter(
        log: log,
        name: 'background',
      ),
      foregroundPainter: TestCustomPainter(
        log: log,
        name: 'foreground',
      ),
      child: CustomPaint(
        painter: TestCustomPainter(
          log: log,
          name: 'child',
        ),
      ),
    ));

    expect(log, equals(<String>['background', 'child', 'foreground']));
  });

  testWidgets('Throws FlutterError on custom painter incorrect restore/save calls', (
      WidgetTester tester) async {
    final GlobalKey target = GlobalKey();
    final List<String> log = <String>[];
    await tester.pumpWidget(CustomPaint(
      key: target,
      isComplex: true,
      painter: TestCustomPainter(log: log),
    ));
    final RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint;
    final Canvas canvas = MockCanvas();
    int saveCount = 0;
    when(canvas.getSaveCount()).thenAnswer((_) => saveCount++);
    final PaintingContext paintingContext = MockPaintingContext();
    when(paintingContext.canvas).thenReturn(canvas);

    FlutterError getError() {
      FlutterError error;
      try {
        renderCustom.paint(paintingContext, const Offset(0, 0));
      } on FlutterError catch (e) {
        error = e;
      }
      return error;
    }

    FlutterError error = getError();
    expect(error.toStringDeep(), equalsIgnoringHashCodes(
      'FlutterError\n'
      '   The TestCustomPainter#00000() custom painter called canvas.save()\n'
      '   or canvas.saveLayer() at least 1 more time than it called\n'
      '   canvas.restore().\n'
      '   This leaves the canvas in an inconsistent state and will probably\n'
      '   result in a broken display.\n'
      '   You must pair each call to save()/saveLayer() with a later\n'
      '   matching call to restore().\n'
    ));

    when(canvas.getSaveCount()).thenAnswer((_) => saveCount--);
    error = getError();
    expect(error.toStringDeep(), equalsIgnoringHashCodes(
      'FlutterError\n'
      '   The TestCustomPainter#00000() custom painter called\n'
      '   canvas.restore() 1 more time than it called canvas.save() or\n'
      '   canvas.saveLayer().\n'
      '   This leaves the canvas in an inconsistent state and will result\n'
      '   in a broken display.\n'
      '   You should only call restore() if you first called save() or\n'
      '   saveLayer().\n'
    ));

    when(canvas.getSaveCount()).thenAnswer((_) => saveCount += 2);
    error = getError();
    expect(error.toStringDeep(), contains('2 more times'));

    when(canvas.getSaveCount()).thenAnswer((_) => saveCount -= 2);
    error = getError();
    expect(error.toStringDeep(), contains('2 more times'));
  });

  testWidgets('assembleSemanticsNode throws FlutterError', (WidgetTester tester) async {
    final List<String> log = <String>[];
    final GlobalKey target = GlobalKey();
    await tester.pumpWidget(CustomPaint(
      key: target,
      isComplex: true,
      painter: TestCustomPainter(log: log),
    ));
    final RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint;
    dynamic error;
    try {
      renderCustom.assembleSemanticsNode(
        null,
        null,
        <SemanticsNode>[SemanticsNode()],
      );
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
    expect(error.toStringDeep(), equalsIgnoringHashCodes(
      'FlutterError\n'
      '   RenderCustomPaint does not have a child widget but received a\n'
      '   non-empty list of child SemanticsNode:\n'
      '   SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible)\n'
    ));

    await tester.pumpWidget(CustomPaint(
      key: target,
      isComplex: true,
      painter: TestCustomPainterWithCustomSemanticsBuilder(),
    ));
    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    error = exception;
    expect(error.toStringDeep(), equalsIgnoringHashCodes(
      'FlutterError\n'
      '   Failed to update the list of CustomPainterSemantics:\n'
      "   - duplicate key [<'0'>] found at position 1\n"
    ));
  });

  testWidgets('CustomPaint sizing', (WidgetTester tester) async {
    final GlobalKey target = GlobalKey();

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target),
    ));
    expect(target.currentContext.size, Size.zero);

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target, child: Container()),
    ));
    expect(target.currentContext.size, const Size(800.0, 600.0));

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target, size: const Size(20.0, 20.0)),
    ));
    expect(target.currentContext.size, const Size(20.0, 20.0));

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target, size: const Size(2000.0, 100.0)),
    ));
    expect(target.currentContext.size, const Size(800.0, 100.0));

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target, size: Size.zero, child: Container()),
    ));
    expect(target.currentContext.size, const Size(800.0, 600.0));

    await tester.pumpWidget(Center(
      child: CustomPaint(key: target, child: const SizedBox(height: 0.0, width: 0.0)),
    ));
    expect(target.currentContext.size, Size.zero);

  });

  testWidgets('Raster cache hints', (WidgetTester tester) async {
    final GlobalKey target = GlobalKey();

    final List<String> log = <String>[];
    await tester.pumpWidget(CustomPaint(
      key: target,
      isComplex: true,
      painter: TestCustomPainter(log: log),
    ));
    RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint;
    expect(renderCustom.isComplex, true);
    expect(renderCustom.willChange, false);

    await tester.pumpWidget(CustomPaint(
      key: target,
      willChange: true,
      foregroundPainter: TestCustomPainter(log: log),
    ));
    renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint;
    expect(renderCustom.isComplex, false);
    expect(renderCustom.willChange, true);
  });

  test('Raster cache hints cannot be set with null painters', () {
    expect(() => CustomPaint(isComplex: true), throwsAssertionError);
    expect(() => CustomPaint(willChange: true), throwsAssertionError);
  });
}
