// Copyright 2016 The Chromium 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:math' as math;
import 'dart:ui';

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

import '../widgets/semantics_tester.dart';

void main() {
  testWidgets('Floating Action Button control test', (WidgetTester tester) async {
    bool didPressButton = false;
    await tester.pumpWidget(
      new Directionality(
        textDirection: TextDirection.ltr,
        child: new Center(
          child: new FloatingActionButton(
            onPressed: () {
              didPressButton = true;
            },
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );

    expect(didPressButton, isFalse);
    await tester.tap(find.byType(Icon));
    expect(didPressButton, isTrue);
  });

  testWidgets('Floating Action Button tooltip', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        home: const Scaffold(
          floatingActionButton: const FloatingActionButton(
            onPressed: null,
            tooltip: 'Add',
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );

    await tester.tap(find.byType(Icon));
    expect(find.byTooltip('Add'), findsOneWidget);
  });

  testWidgets('Floating Action Button tooltip (no child)', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        home: const Scaffold(
          floatingActionButton: const FloatingActionButton(
            onPressed: null,
            tooltip: 'Add',
          ),
        ),
      ),
    );

    expect(find.byType(Text), findsNothing);
    await tester.longPress(find.byType(FloatingActionButton));
    await tester.pump();
    expect(find.byType(Text), findsOneWidget);
  });

  testWidgets('Floating Action Button heroTag', (WidgetTester tester) async {
    BuildContext theContext;
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) {
              theContext = context;
              return const FloatingActionButton(heroTag: 1, onPressed: null);
            },
          ),
          floatingActionButton: const FloatingActionButton(heroTag: 2, onPressed: null),
        ),
      ),
    );
    Navigator.push(theContext, new PageRouteBuilder<Null>(
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        return const Placeholder();
      },
    ));
    await tester.pump(); // this would fail if heroTag was the same on both FloatingActionButtons (see below).
  });

  testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async {
    BuildContext theContext;
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) {
              theContext = context;
              return const FloatingActionButton(onPressed: null);
            },
          ),
          floatingActionButton: const FloatingActionButton(onPressed: null),
        ),
      ),
    );
    Navigator.push(theContext, new PageRouteBuilder<Null>(
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        return const Placeholder();
      },
    ));
    await tester.pump();
    expect(tester.takeException().toString(), contains('FloatingActionButton'));
  });

  testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async {
    BuildContext theContext;
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) {
              theContext = context;
              return const FloatingActionButton(heroTag: 'xyzzy', onPressed: null);
            },
          ),
          floatingActionButton: const FloatingActionButton(heroTag: 'xyzzy', onPressed: null),
        ),
      ),
    );
    Navigator.push(theContext, new PageRouteBuilder<Null>(
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        return const Placeholder();
      },
    ));
    await tester.pump();
    expect(tester.takeException().toString(), contains('xyzzy'));
  });

  testWidgets('Floating Action Button semantics (enabled)', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);

    await tester.pumpWidget(
      new Directionality(
        textDirection: TextDirection.ltr,
        child: new Center(
          child: new FloatingActionButton(
            onPressed: () { },
            child: const Icon(Icons.add, semanticLabel: 'Add'),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(new TestSemantics.root(
      children: <TestSemantics>[
        new TestSemantics.rootChild(
          label: 'Add',
          flags: <SemanticsFlag>[
            SemanticsFlag.isButton,
            SemanticsFlag.hasEnabledState,
            SemanticsFlag.isEnabled,
          ],
          actions: <SemanticsAction>[
            SemanticsAction.tap
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreId: true, ignoreRect: true));

    semantics.dispose();
  });

  testWidgets('Floating Action Button semantics (disabled)', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);

    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: const Center(
          child: const FloatingActionButton(
            onPressed: null,
            child: const Icon(Icons.add, semanticLabel: 'Add'),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(new TestSemantics.root(
      children: <TestSemantics>[
        new TestSemantics.rootChild(
          label: 'Add',
          flags: <SemanticsFlag>[
            SemanticsFlag.isButton,
            SemanticsFlag.hasEnabledState,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreId: true, ignoreRect: true));

    semantics.dispose();
  });

  group('ComputeNotch', () {
    testWidgets('host and guest must intersect', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTWH(50.0, 50.0, 10.0, 10.0);
      final Offset start = const Offset(10.0, 100.0);
      final Offset end = const Offset(60.0, 100.0);
      expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
    });

    testWidgets('start/end must be on top edge', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);

      Offset start = const Offset(180.0, 100.0);
      Offset end = const Offset(220.0, 110.0);
      expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);

      start = const Offset(180.0, 110.0);
      end = const Offset(220.0, 100.0);
      expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
    });

    testWidgets('start must be to the left of the notch', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);

      final Offset start = const Offset(191.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);
      expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
    });

    testWidgets('end must be to the right of the notch', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);

      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(209.0, 100.0);
      expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
    });

    testWidgets('notch no margin', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null, notchMargin: 0.0));
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);

      final Path actualNotch = computeNotch(host, guest, start, end);
      final Path notchedRectangle =
        createNotchedRectangle(host, start.dx, end.dx, actualNotch);

      expect(pathDoesNotContainCircle(notchedRectangle, guest), isTrue);
    });

    testWidgets('notch with margin', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester,
	const FloatingActionButton(onPressed: null, notchMargin: 4.0)
      );
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);

      final Path actualNotch = computeNotch(host, guest, start, end);
      final Path notchedRectangle =
        createNotchedRectangle(host, start.dx, end.dx, actualNotch);
      expect(pathDoesNotContainCircle(notchedRectangle, guest.inflate(4.0)), isTrue);
    });

    testWidgets('notch circle center above BAB', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester,
	const FloatingActionButton(onPressed: null, notchMargin: 4.0)
      );
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 85.0, 210.0, 105.0);
      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);

      final Path actualNotch = computeNotch(host, guest, start, end);
      final Path notchedRectangle =
        createNotchedRectangle(host, start.dx, end.dx, actualNotch);
      expect(pathDoesNotContainCircle(notchedRectangle, guest.inflate(4.0)), isTrue);
    });

    testWidgets('notch circle center below BAB', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester,
	const FloatingActionButton(onPressed: null, notchMargin: 4.0)
      );
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 95.0, 210.0, 115.0);
      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);

      final Path actualNotch = computeNotch(host, guest, start, end);
      final Path notchedRectangle =
        createNotchedRectangle(host, start.dx, end.dx, actualNotch);
      expect(pathDoesNotContainCircle(notchedRectangle, guest.inflate(4.0)), isTrue);
    });

    testWidgets('no notch when there is no overlap', (WidgetTester tester) async {
      final ComputeNotch computeNotch = await fetchComputeNotch(tester,
	const FloatingActionButton(onPressed: null, notchMargin: 4.0)
      );
      final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
      final Rect guest = new Rect.fromLTRB(190.0, 40.0, 210.0, 60.0);
      final Offset start = const Offset(180.0, 100.0);
      final Offset end = const Offset(220.0, 100.0);

      final Path actualNotch = computeNotch(host, guest, start, end);
      final Path notchedRectangle =
        createNotchedRectangle(host, start.dx, end.dx, actualNotch);
      expect(pathDoesNotContainCircle(notchedRectangle, guest.inflate(4.0)), isTrue);
    });

  });

}

Path createNotchedRectangle(Rect container, double startX, double endX, Path notch) {
  return new Path()
    ..moveTo(container.left, container.top)
    ..lineTo(startX, container.top)
    ..addPath(notch, Offset.zero)
    ..lineTo(container.right, container.top)
    ..lineTo(container.right, container.bottom)
    ..lineTo(container.left, container.bottom)
    ..close();
}
Future<ComputeNotch> fetchComputeNotch(WidgetTester tester, FloatingActionButton fab) async {
      await tester.pumpWidget(new MaterialApp(
          home: new Scaffold(
            body: new ConstrainedBox(
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new GeometryListener(),
            ),
            floatingActionButton: fab,
          )
      ));
      final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
      return listenerState.cache.value.floatingActionButtonNotch;
}

class GeometryListener extends StatefulWidget {
  @override
  State createState() => new GeometryListenerState();
}

class GeometryListenerState extends State<GeometryListener> {
  @override
  Widget build(BuildContext context) {
    return new CustomPaint(
      painter: cache
    );
  }

  ValueListenable<ScaffoldGeometry> geometryListenable;
  GeometryCachePainter cache;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
    if (geometryListenable == newListenable)
      return;
    
    geometryListenable = newListenable;
    cache = new GeometryCachePainter(geometryListenable);
  }

}

// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class GeometryCachePainter extends CustomPainter {
  GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);

  final ValueListenable<ScaffoldGeometry> geometryListenable;

  ScaffoldGeometry value;
  @override
  void paint(Canvas canvas, Size size) {
    value = geometryListenable.value;
  }

  @override
  bool shouldRepaint(GeometryCachePainter oldDelegate) {
    return true;
  }
}

bool pathDoesNotContainCircle(Path path, Rect circleBounds) {
  assert(circleBounds.width == circleBounds.height);
  final double radius = circleBounds.width / 2.0;

  for (double theta = 0.0; theta <= 2.0 * math.pi; theta += math.pi / 20.0) {
    for (double i = 0.0; i < 1; i += 0.01) {
      final double x = i * radius * math.cos(theta);
      final double y = i * radius * math.sin(theta);
      if (path.contains(new Offset(x,y) + circleBounds.center))
        return false;
    }
  }
  return true;
}