// 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 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

import 'semantics_tester.dart';

void main() {
  testWidgetsWithLeakTracking('Semantics can merge sibling group', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const SemanticsTag first = SemanticsTag('1');
    const SemanticsTag second = SemanticsTag('2');
    const SemanticsTag third = SemanticsTag('3');
    ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
      expect(configs.length, 3);
      final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
      final List<SemanticsConfiguration> sibling = <SemanticsConfiguration>[];
      // Merge first and third
      for (final SemanticsConfiguration config in configs) {
        if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
          sibling.add(config);
        } else {
          builder.markAsMergeUp(config);
        }
      }
      builder.markAsSiblingMergeGroup(sibling);
      return builder.build();
    }
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'parent',
          child: TestConfigDelegate(
            delegate: delegate,
            child: Column(
              children: <Widget>[
                Semantics(
                  label: '1',
                  tagForChildren: first,
                  child: const SizedBox(width: 100, height: 100),
                  // this tests that empty nodes disappear
                ),
                Semantics(
                  label: '2',
                  tagForChildren: second,
                  child: const SizedBox(width: 100, height: 100),
                ),
                Semantics(
                  label: '3',
                  tagForChildren: third,
                  child: const SizedBox(width: 100, height: 100),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'parent\n2',
        ),
        TestSemantics.rootChild(
          label: '1\n3',
        ),
      ],
    ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
    semantics.dispose();
  });

  testWidgetsWithLeakTracking('Semantics can drop semantics config', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const SemanticsTag first = SemanticsTag('1');
    const SemanticsTag second = SemanticsTag('2');
    const SemanticsTag third = SemanticsTag('3');
    ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
      final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
      // Merge first and third
      for (final SemanticsConfiguration config in configs) {
        if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
          continue;
        }
        builder.markAsMergeUp(config);
      }
      return builder.build();
    }
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'parent',
          child: TestConfigDelegate(
            delegate: delegate,
            child: Column(
              children: <Widget>[
                Semantics(
                  label: '1',
                  tagForChildren: first,
                  child: const SizedBox(width: 100, height: 100),
                  // this tests that empty nodes disappear
                ),
                Semantics(
                  label: '2',
                  tagForChildren: second,
                  child: const SizedBox(width: 100, height: 100),
                ),
                Semantics(
                  label: '3',
                  tagForChildren: third,
                  child: const SizedBox(width: 100, height: 100),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'parent\n2',
        ),
      ],
    ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
    semantics.dispose();
  });

  testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async {
    const SemanticsTag first = SemanticsTag('1');
    const SemanticsTag second = SemanticsTag('2');
    const SemanticsTag third = SemanticsTag('3');
    ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
      final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
      // Marks the same one twice.
      builder.markAsMergeUp(configs.first);
      builder.markAsMergeUp(configs.first);
      return builder.build();
    }
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'parent',
          child: TestConfigDelegate(
            delegate: delegate,
            child: Column(
              children: <Widget>[
                Semantics(
                  label: '1',
                  tagForChildren: first,
                  child: const SizedBox(width: 100, height: 100),
                  // this tests that empty nodes disappear
                ),
                Semantics(
                  label: '2',
                  tagForChildren: second,
                  child: const SizedBox(width: 100, height: 100),
                ),
                Semantics(
                  label: '3',
                  tagForChildren: third,
                  child: const SizedBox(width: 100, height: 100),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isAssertionError);
  });

  testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async {
    const SemanticsTag first = SemanticsTag('1');
    const SemanticsTag second = SemanticsTag('2');
    const SemanticsTag third = SemanticsTag('3');
    ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
      final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
      // Marks the same one twice.
      builder.markAsMergeUp(configs.first);
      builder.markAsSiblingMergeGroup(<SemanticsConfiguration>[configs.first]);
      return builder.build();
    }
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'parent',
          child: TestConfigDelegate(
            delegate: delegate,
            child: Column(
              children: <Widget>[
                Semantics(
                  label: '1',
                  tagForChildren: first,
                  child: const SizedBox(width: 100, height: 100),
                  // this tests that empty nodes disappear
                ),
                Semantics(
                  label: '2',
                  tagForChildren: second,
                  child: const SizedBox(width: 100, height: 100),
                ),
                Semantics(
                  label: '3',
                  tagForChildren: third,
                  child: const SizedBox(width: 100, height: 100),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isAssertionError);
  });

  testWidgetsWithLeakTracking('RenderObject with semantics child delegate will mark correct boundary dirty', (WidgetTester tester) async {
    final UniqueKey inner = UniqueKey();
    final UniqueKey boundaryParent = UniqueKey();
    final UniqueKey grandBoundaryParent = UniqueKey();
    ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
      final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
      configs.forEach(builder.markAsMergeUp);
      return builder.build();
    }
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: MarkSemanticsDirtySpy(
          key: grandBoundaryParent,
          child: MarkSemanticsDirtySpy(
            key: boundaryParent,
            child: TestConfigDelegate(
              delegate: delegate,
              child: Column(
                children: <Widget>[
                  Semantics(
                    label: 'label',
                    child: MarkSemanticsDirtySpy(
                      key: inner,
                      child: const Text('inner'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );

    final RenderMarkSemanticsDirtySpy innerObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(inner));
    final RenderTestConfigDelegate objectWithDelegate = tester.renderObject<RenderTestConfigDelegate>(find.byType(TestConfigDelegate));
    final RenderMarkSemanticsDirtySpy boundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(boundaryParent));
    final RenderMarkSemanticsDirtySpy grandBoundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(grandBoundaryParent));
    void resetBuildState() {
      innerObject.hasRebuildSemantics = false;
      boundaryParentObject.hasRebuildSemantics = false;
      grandBoundaryParentObject.hasRebuildSemantics = false;
    }
    // Sanity check
    expect(innerObject.hasRebuildSemantics, isTrue);
    expect(boundaryParentObject.hasRebuildSemantics, isTrue);
    expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue);
    resetBuildState();

    innerObject.markNeedsSemanticsUpdate();
    await tester.pump();
    // Inner boundary should not trigger rebuild above it.
    expect(innerObject.hasRebuildSemantics, isTrue);
    expect(boundaryParentObject.hasRebuildSemantics, isFalse);
    expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse);
    resetBuildState();

    objectWithDelegate.markNeedsSemanticsUpdate();
    await tester.pump();
    // object with delegate rebuilds up to grand parent boundary;
    expect(innerObject.hasRebuildSemantics, isTrue);
    expect(boundaryParentObject.hasRebuildSemantics, isTrue);
    expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue);
    resetBuildState();

    boundaryParentObject.markNeedsSemanticsUpdate();
    await tester.pump();
    // Render objects in between child delegate and grand boundary parent does
    // not mark the grand boundary parent dirty because it should not change the
    // generated sibling nodes.
    expect(innerObject.hasRebuildSemantics, isTrue);
    expect(boundaryParentObject.hasRebuildSemantics, isTrue);
    expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse);
  });
}

class MarkSemanticsDirtySpy extends SingleChildRenderObjectWidget {
  const MarkSemanticsDirtySpy({super.key, super.child});
  @override
  RenderMarkSemanticsDirtySpy createRenderObject(BuildContext context) => RenderMarkSemanticsDirtySpy();
}

class RenderMarkSemanticsDirtySpy extends RenderProxyBox {
  RenderMarkSemanticsDirtySpy();
  bool hasRebuildSemantics = false;

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    config.isSemanticBoundary = true;
  }

  @override
  void assembleSemanticsNode(
    SemanticsNode node,
    SemanticsConfiguration config,
    Iterable<SemanticsNode> children,
  ) {
    hasRebuildSemantics = true;
  }
}

class TestConfigDelegate extends SingleChildRenderObjectWidget {
  const TestConfigDelegate({super.key, required this.delegate, super.child});
  final ChildSemanticsConfigurationsDelegate delegate;

  @override
  RenderTestConfigDelegate createRenderObject(BuildContext context) => RenderTestConfigDelegate(
    delegate: delegate,
  );

  @override
  void updateRenderObject(BuildContext context, RenderTestConfigDelegate renderObject) {
    renderObject.delegate = delegate;
  }
}

class RenderTestConfigDelegate extends RenderProxyBox {
  RenderTestConfigDelegate({
    ChildSemanticsConfigurationsDelegate? delegate,
  }) : _delegate = delegate;

  ChildSemanticsConfigurationsDelegate? get delegate => _delegate;
  ChildSemanticsConfigurationsDelegate? _delegate;
  set delegate(ChildSemanticsConfigurationsDelegate? value) {
    if (value != _delegate) {
      markNeedsSemanticsUpdate();
    }
    _delegate = value;
  }

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    config.childConfigurationsDelegate = delegate;
  }
}