// 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:collection';
import 'dart:math' as math;

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

import 'semantics_tester.dart';

typedef TraversalTestFunction = Future<void> Function(TraversalTester tester);
const Size tenByTen = Size(10.0, 10.0);

void main() {
  setUp(() {
    debugResetSemanticsIdCounter();
  });

  void testTraversal(String description, TraversalTestFunction testFunction) {
    testWidgets(description, (WidgetTester tester) async {
      final TraversalTester traversalTester = TraversalTester(tester);
      await testFunction(traversalTester);
      traversalTester.dispose();
    });
  }

  // ┌───┐ ┌───┐
  // │ A │>│ B │
  // └───┘ └───┘
  testTraversal('Semantics traverses horizontally left-to-right', (TraversalTester tester) async {
    await tester.test(
      textDirection: TextDirection.ltr,
      children: <String, Rect>{
        'A': Offset.zero & tenByTen,
        'B': const Offset(20.0, 0.0) & tenByTen,
      },
      expectedTraversal: 'A B',
    );
  });

  // ┌───┐ ┌───┐
  // │ A │<│ B │
  // └───┘ └───┘
  testTraversal('Semantics traverses horizontally right-to-left', (TraversalTester tester) async {
    await tester.test(
      textDirection: TextDirection.rtl,
      children: <String, Rect>{
        'A': Offset.zero & tenByTen,
        'B': const Offset(20.0, 0.0) & tenByTen,
      },
      expectedTraversal: 'B A',
    );
  });

  // ┌───┐
  // │ A │
  // └───┘
  //   V
  // ┌───┐
  // │ B │
  // └───┘
  testTraversal('Semantics traverses vertically top-to-bottom', (TraversalTester tester) async {
    for (final TextDirection textDirection in TextDirection.values) {
      await tester.test(
        textDirection: textDirection,
        children: <String, Rect>{
          'A': Offset.zero & tenByTen,
          'B': const Offset(0.0, 20.0) & tenByTen,
        },
        expectedTraversal: 'A B',
      );
    }
  });

  // ┌───┐ ┌───┐
  // │ A │>│ B │
  // └───┘ └───┘
  //   ┌─────┘
  //   V
  // ┌───┐ ┌───┐
  // │ C │>│ D │
  // └───┘ └───┘
  testTraversal('Semantics traverses a grid left-to-right', (TraversalTester tester) async {
    await tester.test(
      textDirection: TextDirection.ltr,
      children: <String, Rect>{
        'A': Offset.zero & tenByTen,
        'B': const Offset(20.0, 0.0) & tenByTen,
        'C': const Offset(0.0, 20.0) & tenByTen,
        'D': const Offset(20.0, 20.0) & tenByTen,
      },
      expectedTraversal: 'A B C D',
    );
  });

  // ┌───┐ ┌───┐
  // │ A │<│ B │
  // └───┘ └───┘
  //   └─────┐
  //         V
  // ┌───┐ ┌───┐
  // │ C │<│ D │
  // └───┘ └───┘
  testTraversal('Semantics traverses a grid right-to-left', (TraversalTester tester) async {
    await tester.test(
      textDirection: TextDirection.rtl,
      children: <String, Rect>{
        'A': Offset.zero & tenByTen,
        'B': const Offset(20.0, 0.0) & tenByTen,
        'C': const Offset(0.0, 20.0) & tenByTen,
        'D': const Offset(20.0, 20.0) & tenByTen,
      },
      expectedTraversal: 'B A D C',
    );
  });

  // ┌───┐           ┌───┐
  // │ A │           │ C │
  // └───┘<->┌───┐<->└───┘
  //         │ B │
  //         └───┘
  testTraversal('Semantics traverses vertically overlapping nodes horizontally', (TraversalTester tester) async {
    final Map<String, Rect> children = <String, Rect>{
      'A': Offset.zero & tenByTen,
      'B': const Offset(20.0, 5.0) & tenByTen,
      'C': const Offset(40.0, 0.0) & tenByTen,
    };

    await tester.test(
      textDirection: TextDirection.ltr,
      children: children,
      expectedTraversal: 'A B C',
    );

    await tester.test(
      textDirection: TextDirection.rtl,
      children: children,
      expectedTraversal: 'C B A',
    );
  });

  // LTR:
  // ┌───┐ ┌───┐ ┌───┐ ┌───┐
  // │ A │>│ B │>│ C │>│ D │
  // └───┘ └───┘ └───┘ └───┘
  //   ┌─────────────────┘
  //   V
  // ┌───┐ ┌─────────┐ ┌───┐
  // │ E │>│         │>│ G │
  // └───┘ │    F    │ └───┘
  //   ┌───|─────────|───┘
  // ┌───┐ │         │ ┌───┐
  // │ H │─|─────────|>│ I │
  // └───┘ └─────────┘ └───┘
  //   ┌─────────────────┘
  //   V
  // ┌───┐ ┌───┐ ┌───┐ ┌───┐
  // │ J │>│ K │>│ L │>│ M │
  // └───┘ └───┘ └───┘ └───┘
  //
  // RTL:
  // ┌───┐ ┌───┐ ┌───┐ ┌───┐
  // │ A │<│ B │<│ C │<│ D │
  // └───┘ └───┘ └───┘ └───┘
  //   └─────────────────┐
  //                     V
  // ┌───┐ ┌─────────┐ ┌───┐
  // │ E │<│         │<│ G │
  // └───┘ │    F    │ └───┘
  //    └──|─────────|────┐
  // ┌───┐ │         │ ┌───┐
  // │ H │<|─────────|─│ I │
  // └───┘ └─────────┘ └───┘
  //   └─────────────────┐
  //                     V
  // ┌───┐ ┌───┐ ┌───┐ ┌───┐
  // │ J │<│ K │<│ L │<│ M │
  // └───┘ └───┘ └───┘ └───┘
  testTraversal('Semantics traverses vertical groups, then horizontal groups, then knots', (TraversalTester tester) async {
    final Map<String, Rect> children = <String, Rect>{
      'A': Offset.zero & tenByTen,
      'B': const Offset(20.0, 0.0) & tenByTen,
      'C': const Offset(40.0, 0.0) & tenByTen,
      'D': const Offset(60.0, 0.0) & tenByTen,
      'E': const Offset(0.0, 20.0) & tenByTen,
      'F': const Offset(20.0, 20.0) & (tenByTen * 2.0),
      'G': const Offset(60.0, 20.0) & tenByTen,
      'H': const Offset(0.0, 40.0) & tenByTen,
      'I': const Offset(60.0, 40.0) & tenByTen,
      'J': const Offset(0.0, 60.0) & tenByTen,
      'K': const Offset(20.0, 60.0) & tenByTen,
      'L': const Offset(40.0, 60.0) & tenByTen,
      'M': const Offset(60.0, 60.0) & tenByTen,
    };

    await tester.test(
      textDirection: TextDirection.ltr,
      children: children,
      expectedTraversal: 'A B C D E F G H I J K L M',
    );

    await tester.test(
      textDirection: TextDirection.rtl,
      children: children,
      expectedTraversal: 'D C B A G F E I H M L K J',
    );
  });

  // The following test tests traversal of the simplest "knot", which is two
  // nodes overlapping both vertically and horizontally. For example:
  //
  // ┌─────────┐
  // │         │
  // │   A     │
  // │     ┌───┼─────┐
  // │     │   │     │
  // └─────┼───┘     │
  //       │     B   │
  //       │         │
  //       └─────────┘
  //
  // The outcome depends on the relative positions of the centers of `Rect`s of
  // their respective boxes, specifically the direction (i.e. angle) of the
  // vector pointing from A to B. We test different angles, one for each octant:
  //
  //  -3π/4 -π/2  -π/4
  //      ╲   │   ╱
  //       ╲ 1│2 ╱
  //        ╲ │ ╱
  //     i=0 ╲│╱ 3
  //  π ──────┼────── 0
  //       7 ╱│╲ 4
  //        ╱ │ ╲
  //       ╱ 6│5 ╲
  //      ╱   │   ╲
  //   3π/4  π/2   π/4
  //
  // For LTR, angles falling into octants 3, 4, 5, and 6, produce A -> B, all
  // others produce B -> A.
  //
  // For RTL, angles falling into octants 5, 6, 7, and 0, produce A -> B, all
  // others produce B -> A.
  testTraversal('Semantics sorts knots', (TraversalTester tester) async {
    const double start = -math.pi + math.pi / 8.0;

    for (int i = 0; i < 8; i += 1) {
      final double angle = start + i.toDouble() * math.pi / 4.0;
      // These values should be truncated so that double precision rounding
      // issues won't impact the heights/widths and throw off the traversal
      // ordering.
      final double dx = (math.cos(angle) * 15.0) / 10.0;
      final double dy = (math.sin(angle) * 15.0) / 10.0;

      final Map<String, Rect> children = <String, Rect>{
        'A': const Offset(10.0, 10.0) & tenByTen,
        'B': Offset(10.0 + dx, 10.0 + dy) & tenByTen,
      };

      try {
        await tester.test(
          textDirection: TextDirection.ltr,
          children: children,
          expectedTraversal: 3 <= i && i <= 6 ? 'A B' : 'B A',
        );

        await tester.test(
          textDirection: TextDirection.rtl,
          children: children,
          expectedTraversal: 1 <= i && i <= 4 ? 'B A' : 'A B',
        );
      } catch (error) {
        fail(
          'Test failed with i == $i, angle == ${angle / math.pi}π\n'
          '$error'
        );
      }
    }
  });
}

class TraversalTester {
  TraversalTester(this.tester) : semantics = SemanticsTester(tester);

  final WidgetTester tester;
  final SemanticsTester semantics;

  Future<void> test({
    required TextDirection textDirection,
    required Map<String, Rect> children,
    required String expectedTraversal,
  }) async {
    assert(children is LinkedHashMap);
    await tester.pumpWidget(
        Directionality(
          textDirection: textDirection,
          child: Semantics(
            textDirection: textDirection,
            child: CustomMultiChildLayout(
              delegate: TestLayoutDelegate(children),
              children: children.keys.map<Widget>((String label) {
                return LayoutId(
                  id: label,
                  child: Semantics(
                    container: true,
                    explicitChildNodes: true,
                    label: label,
                    child: SizedBox(
                      width: children[label]!.width,
                      height: children[label]!.height,
                    ),
                  ),
                );
              }).toList(),
            ),
          ),
        ),
    );

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics.rootChild(
            textDirection: textDirection,
            children: expectedTraversal.split(' ').map<TestSemantics>((String label) {
              return TestSemantics(
                label: label,
              );
            }).toList(),
          ),
        ],
      ),
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
      childOrder: DebugSemanticsDumpOrder.traversalOrder,
    ));
  }

  void dispose() {
    semantics.dispose();
  }
}

class TestLayoutDelegate extends MultiChildLayoutDelegate {

  TestLayoutDelegate(this.children);

  final Map<String, Rect> children;

  @override
  void performLayout(Size size) {
    children.forEach((String label, Rect rect) {
      layoutChild(label, BoxConstraints.loose(size));
      positionChild(label, rect.topLeft);
    });
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => oldDelegate == this;
}
