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

import 'dart:async';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui show Image;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

import '../painting/image_data.dart';
import '../rendering/mock_canvas.dart';

class TestImageProvider extends ImageProvider<TestImageProvider> {
  TestImageProvider(this.future);

  final Future<void> future;

  static ui.Image image;

  @override
  Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<TestImageProvider>(this);
  }

  @override
  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
    return OneFrameImageStreamCompleter(
      future.then<ImageInfo>((void value) => ImageInfo(image: image))
    );
  }
}

Future<void> main() async {
  AutomatedTestWidgetsFlutterBinding();
  TestImageProvider.image = await decodeImageFromList(Uint8List.fromList(kTransparentImage));

  testWidgets('DecoratedBox handles loading images', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    final Completer<void> completer = Completer<void>();
    await tester.pumpWidget(
      KeyedSubtree(
        key: key,
        child: DecoratedBox(
          decoration: BoxDecoration(
            image: DecorationImage(
              image: TestImageProvider(completer.future),
            ),
          ),
        ),
      ),
    );
    expect(tester.binding.hasScheduledFrame, isFalse);
    completer.complete();
    await tester.idle();
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(tester.binding.hasScheduledFrame, isFalse);
  });

  testWidgets('Moving a DecoratedBox', (WidgetTester tester) async {
    final Completer<void> completer = Completer<void>();
    final Widget subtree = KeyedSubtree(
      key: GlobalKey(),
      child: RepaintBoundary(
        child: DecoratedBox(
          decoration: BoxDecoration(
            image: DecorationImage(
              image: TestImageProvider(completer.future),
            ),
          ),
        ),
      ),
    );
    await tester.pumpWidget(subtree);
    await tester.idle();
    expect(tester.binding.hasScheduledFrame, isFalse);
    await tester.pumpWidget(Container(child: subtree));
    await tester.idle();
    expect(tester.binding.hasScheduledFrame, isFalse);
    completer.complete(); // schedules microtask, does not run it
    expect(tester.binding.hasScheduledFrame, isFalse);
    await tester.idle(); // runs microtask
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    await tester.idle();
    expect(tester.binding.hasScheduledFrame, isFalse);
  });

  testWidgets('Circles can have uniform borders', (WidgetTester tester) async {
    await tester.pumpWidget(
      Container(
        padding: const EdgeInsets.all(50.0),
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          border: Border.all(width: 10.0, color: const Color(0x80FF00FF)),
          color: Colors.teal[600],
        ),
      ),
    );
  });

  testWidgets('Bordered Container insets its child', (WidgetTester tester) async {
    const Key key = Key('outerContainer');
    await tester.pumpWidget(
      Center(
        child: Container(
          key: key,
          decoration: BoxDecoration(border: Border.all(width: 10.0)),
          child: Container(
            width: 25.0,
            height: 25.0,
          ),
        ),
      ),
    );
    expect(tester.getSize(find.byKey(key)), equals(const Size(45.0, 45.0)));
  });

  testWidgets('BoxDecoration paints its border correctly', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/7672

    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      return Center(
        child: Container(
          key: key,
          width: 100.0,
          height: 50.0,
          decoration: BoxDecoration(border: border),
        ),
      );
    }

    const Color black = Color(0xFF000000);

    await tester.pumpWidget(buildFrame(Border.all()));
    expect(find.byKey(key), paints
      ..rect(color: black, style: PaintingStyle.stroke, strokeWidth: 1.0));

    await tester.pumpWidget(buildFrame(Border.all(width: 0.0)));
    expect(find.byKey(key), paints
      ..rect(color: black, style: PaintingStyle.stroke, strokeWidth: 0.0));

    const Color green = Color(0xFF00FF00);
    const BorderSide greenSide = BorderSide(color: green, width: 10.0);

    await tester.pumpWidget(buildFrame(const Border(top: greenSide)));
    expect(find.byKey(key), paints..path(color: green, style: PaintingStyle.fill));

    await tester.pumpWidget(buildFrame(const Border(left: greenSide)));
    expect(find.byKey(key), paints..path(color: green, style: PaintingStyle.fill));

    await tester.pumpWidget(buildFrame(const Border(right: greenSide)));
    expect(find.byKey(key), paints..path(color: green, style: PaintingStyle.fill));

    await tester.pumpWidget(buildFrame(const Border(bottom: greenSide)));
    expect(find.byKey(key), paints..path(color: green, style: PaintingStyle.fill));

    const Color blue = Color(0xFF0000FF);
    const BorderSide blueSide = BorderSide(color: blue, width: 0.0);

    await tester.pumpWidget(buildFrame(const Border(top: blueSide, right: greenSide, bottom: greenSide)));
    expect(find.byKey(key), paints
      ..path() // There's not much point checking the arguments to these calls because paintBorder
      ..path() // reuses the same Paint object each time, configured differently, and so they will
      ..path()); // all appear to have the same settings here (that of the last call).
  });

  testWidgets('BoxDecoration paints its border correctly', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/12165
    await tester.pumpWidget(
      Column(
        children: <Widget>[
          Container(
            // There's not currently a way to verify that this paints the same size as the others,
            // so the pattern below just asserts that there's four paths but doesn't check the geometry.
            width: 100.0,
            height: 100.0,
            decoration: const BoxDecoration(
              border: Border(
                top: BorderSide(
                  width: 10.0,
                  color: Color(0xFFEEEEEE),
                ),
                left: BorderSide(
                  width: 10.0,
                  color: Color(0xFFFFFFFF),
                ),
                right: BorderSide(
                  width: 10.0,
                  color: Color(0xFFFFFFFF),
                ),
                bottom: BorderSide(
                  width: 10.0,
                  color: Color(0xFFFFFFFF),
                ),
              ),
            ),
          ),
          Container(
            width: 100.0,
            height: 100.0,
            decoration: BoxDecoration(
              border: Border.all(
                width: 10.0,
                color: const Color(0xFFFFFFFF),
              ),
            ),
          ),
          Container(
            width: 100.0,
            height: 100.0,
            decoration: BoxDecoration(
              border: Border.all(
                width: 10.0,
                color: const Color(0xFFFFFFFF),
              ),
              borderRadius: const BorderRadius.only(
                topRight: Radius.circular(10.0),
              ),
            ),
          ),
          Container(
            width: 100.0,
            height: 100.0,
            decoration: BoxDecoration(
              border: Border.all(
                width: 10.0,
                color: const Color(0xFFFFFFFF),
              ),
              shape: BoxShape.circle,
            ),
          ),
        ],
      ),
    );
    expect(find.byType(Column), paints
      ..path()
      ..path()
      ..path()
      ..path()
      ..rect(rect: const Rect.fromLTRB(355.0, 105.0, 445.0, 195.0))
      ..drrect(
        outer: RRect.fromLTRBAndCorners(
          350.0, 200.0, 450.0, 300.0,
          topLeft: Radius.zero,
          topRight: const Radius.circular(10.0),
          bottomRight: Radius.zero,
          bottomLeft: Radius.zero,
        ),
        inner: RRect.fromLTRBAndCorners(
          360.0, 210.0, 440.0, 290.0,
          topLeft: const Radius.circular(-10.0),
          topRight: Radius.zero,
          bottomRight: const Radius.circular(-10.0),
          bottomLeft: const Radius.circular(-10.0),
        ),
      )
      ..circle(x: 400.0, y: 350.0, radius: 45.0),
    );
  });

  testWidgets('Can hit test on BoxDecoration', (WidgetTester tester) async {

    List<int> itemsTapped;

    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Center(
        child: GestureDetector(
          behavior: HitTestBehavior.deferToChild,
          child: Container(
            key: key,
            width: 100.0,
            height: 50.0,
            decoration: BoxDecoration(border: border),
          ),
          onTap: () {
            itemsTapped.add(1);
          },
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));
    expect(itemsTapped, isEmpty);

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1]);

    await tester.tapAt(const Offset(350.0, 275.0));
    expect(itemsTapped, <int>[1,1]);

    await tester.tapAt(const Offset(449.0, 324.0));
    expect(itemsTapped, <int>[1,1,1]);

  });

  testWidgets('Can hit test on BoxDecoration circle', (WidgetTester tester) async {

    List<int> itemsTapped;

    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Center(
        child: GestureDetector(
            behavior: HitTestBehavior.deferToChild,
            child: Container(
            key: key,
            width: 100.0,
            height: 50.0,
            decoration: BoxDecoration(border: border, shape: BoxShape.circle),
          ),
          onTap: () {
            itemsTapped.add(1);
          },
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));
    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(0.0, 0.0));
    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(350.0, 275.0));
    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(400.0, 300.0));
    expect(itemsTapped, <int>[1]);

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1,1]);

  });

  testWidgets('Can hit test on BoxDecoration border', (WidgetTester tester) async {
    List<int> itemsTapped;
    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Center(
        child: GestureDetector(
          behavior: HitTestBehavior.deferToChild,
          child: Container(
            key: key,
            width: 100.0,
            height: 50.0,
            decoration: BoxDecoration(border: border, shape: BoxShape.rectangle, borderRadius: BorderRadius.circular(20.0)),
          ),
          onTap: () {
            itemsTapped.add(1);
          },
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));

    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(0.0, 0.0));
    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(350.0, 275.0));
    expect(itemsTapped, isEmpty);

    await tester.tapAt(const Offset(400.0, 300.0));
    expect(itemsTapped, <int>[1]);

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1,1]);
  });

  testWidgets('BoxDecoration not tap outside rounded angles - Top Left', (WidgetTester tester) async {
    const double height = 50.0;
    const double width = 50.0;
    const double radius = 12.3;

    List<int> itemsTapped;
    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Align(
        alignment: Alignment.topLeft,
        child:GestureDetector(
          behavior: HitTestBehavior.deferToChild,
          child: Container(
            key: key,
            width: width,
            height: height,
            decoration: BoxDecoration(border: border, shape: BoxShape.rectangle,borderRadius: BorderRadius.circular(radius))
          ),
          onTap: () {
            itemsTapped.add(1);
          },
        )
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));

    expect(itemsTapped, isEmpty);
    // x, y
    const Offset topLeft = Offset(0.0, 0.0);
    const Offset borderTopTangent = Offset(radius-1, 0.0);
    const Offset borderLeftTangent = Offset(0.0,radius-1);
    //the borderDiagonalOffset is the backslash line
    //\\######@@@
    //#\\###@####
    //##\\@######
    //##@########
    //@##########
    //@##########
    const double borderDiagonalOffset = radius - radius * math.sqrt1_2;
    const Offset fartherBorderRadiusPoint = Offset(borderDiagonalOffset,borderDiagonalOffset);

    await tester.tapAt(topLeft);
    expect(itemsTapped, isEmpty,reason: 'top left tapped');

    await tester.tapAt(borderTopTangent);
    expect(itemsTapped, isEmpty,reason: 'border top tapped');

    await tester.tapAt(borderLeftTangent);
    expect(itemsTapped, isEmpty,reason: 'border left tapped');

    await tester.tapAt(fartherBorderRadiusPoint);
    expect(itemsTapped, isEmpty,reason: 'border center tapped');

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1]);

  });

  testWidgets('BoxDecoration tap inside rounded angles - Top Left', (WidgetTester tester) async {
    const double height = 50.0;
    const double width = 50.0;
    const double radius = 12.3;

    List<int> itemsTapped;
    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Align(
          alignment: Alignment.topLeft,
          child:GestureDetector(
            behavior: HitTestBehavior.deferToChild,
            child: Container(
                key: key,
                width: width,
                height: height,
                decoration: BoxDecoration(border: border, shape: BoxShape.rectangle,borderRadius: BorderRadius.circular(radius))
            ),
            onTap: () {
              itemsTapped.add(1);
            },
          )
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));

    expect(itemsTapped, isEmpty);
    // x, y
    const Offset borderTopTangent = Offset(radius, 0.0);
    const Offset borderLeftTangent = Offset(0.0,radius);
    const double borderDiagonalOffset = radius - radius * math.sqrt1_2;
    const Offset fartherBorderRadiusPoint = Offset(borderDiagonalOffset+1,borderDiagonalOffset+1);

    await tester.tapAt(borderTopTangent);
    expect(itemsTapped, <int>[1],reason: 'border Top not tapped');

    await tester.tapAt(borderLeftTangent);
    expect(itemsTapped, <int>[1,1],reason: 'border Left not tapped');

    await tester.tapAt(fartherBorderRadiusPoint);
    expect(itemsTapped, <int>[1,1,1],reason: 'border center not tapped');

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1,1,1,1]);
  });

  testWidgets('BoxDecoration rounded angles other corner works', (WidgetTester tester) async {
    const double height = 50.0;
    const double width = 50.0;
    const double radius = 20;

    List<int> itemsTapped;
    const Key key = Key('Container with BoxDecoration');
    Widget buildFrame(Border border) {
      itemsTapped = <int>[];
      return Align(
          alignment: Alignment.topLeft,
          child:GestureDetector(
            behavior: HitTestBehavior.deferToChild,
            child: Container(
                key: key,
                width: width,
                height: height,
                decoration: BoxDecoration(border: border, shape: BoxShape.rectangle,borderRadius: BorderRadius.circular(radius))
            ),
            onTap: () {
              itemsTapped.add(1);
            },
          )
      );
    }

    await tester.pumpWidget(buildFrame(Border.all()));

    expect(itemsTapped, isEmpty);

    await tester.tap(find.byKey(key));
    expect(itemsTapped, <int>[1]);

    // x, y
    const Offset topRightOutside = Offset(width, 0.0);
    const Offset topRightInside = Offset(width-radius, radius);
    const Offset bottomRightOutside = Offset(width, height);
    const Offset bottomRightInside = Offset(width-radius, height-radius);
    const Offset bottomLeftOutside = Offset(0, height);
    const Offset bottomLeftInside = Offset(radius, height-radius);
    const Offset topLeftOutside = Offset(0, 0);
    const Offset topLeftInside = Offset(radius, radius);

    await tester.tapAt(topRightInside);
    expect(itemsTapped, <int>[1,1],reason: 'top right not tapped');

    await tester.tapAt(topRightOutside);
    expect(itemsTapped, <int>[1,1],reason: 'top right tapped');

    await tester.tapAt(bottomRightInside);
    expect(itemsTapped, <int>[1,1,1],reason: 'bottom right not tapped');

    await tester.tapAt(bottomRightOutside);
    expect(itemsTapped, <int>[1,1,1],reason: 'bottom right tapped');

    await tester.tapAt(bottomLeftInside);
    expect(itemsTapped, <int>[1,1,1,1],reason: 'bottom left not tapped');

    await tester.tapAt(bottomLeftOutside);
    expect(itemsTapped, <int>[1,1,1,1],reason: 'bottom left tapped');

    await tester.tapAt(topLeftInside);
    expect(itemsTapped, <int>[1,1,1,1,1],reason: 'top left not tapped');

    await tester.tapAt(topLeftOutside);
    expect(itemsTapped, <int>[1,1,1,1,1],reason: 'top left tapped');
  });
}