// 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_test/flutter_test.dart';

import 'rendering_tester.dart';

class RenderLayoutTestBox extends RenderProxyBox {
  RenderLayoutTestBox(this.onLayout, {
    this.onPerformLayout,
  });

  final VoidCallback onLayout;
  final VoidCallback? onPerformLayout;

  @override
  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // Doing this in tests is ok, but if you're writing your own
    // render object, you want to override performLayout(), not
    // layout(). Overriding layout() would remove many critical
    // performance optimizations of the rendering system, as well as
    // many bypassing many checked-mode integrity checks.
    super.layout(constraints, parentUsesSize: parentUsesSize);
    onLayout();
  }

  @override
  bool get sizedByParent => true;

  @override
  void performLayout() {
    child?.layout(constraints, parentUsesSize: true);
    onPerformLayout?.call();
  }
}

void main() {
  TestRenderingFlutterBinding.ensureInitialized();

  test('moving children', () {
    RenderBox child1, child2;
    bool movedChild1 = false;
    bool movedChild2 = false;
    final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
    block.add(child1 = RenderLayoutTestBox(() { movedChild1 = true; }));
    block.add(child2 = RenderLayoutTestBox(() { movedChild2 = true; }));

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    layout(block);
    expect(movedChild1, isTrue);
    expect(movedChild2, isTrue);

    movedChild1 = false;
    movedChild2 = false;

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);

    block.move(child1, after: child2);
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isTrue);
    expect(movedChild2, isTrue);

    movedChild1 = false;
    movedChild2 = false;

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
  });

  group('Throws when illegal mutations are attempted: ', () {
    FlutterError catchLayoutError(RenderBox box) {
      Object? error;
      layout(box, onErrors: () {
        error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception;
      });
      expect(error, isFlutterError);
      return error! as FlutterError;
    }

    test('on disposed render objects', () {
      final RenderBox box = RenderLayoutTestBox(() {});
      box.dispose();

      Object? error;
      try {
        box.markNeedsLayout();
      } catch (e) {
        error = e;
      }

      expect(error, isFlutterError);
      expect(
        (error! as FlutterError).message,
        equalsIgnoringWhitespace(
          'A disposed RenderObject was mutated.\n'
          'The disposed RenderObject was:\n'
          '${box.toStringShort()}'
        )
      );
    });

    test('marking itself dirty in performLayout', () {
      late RenderBox child1;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in its own performLayout implementation.\n'
          'A RenderObject must not re-dirty itself while still being laid out.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'Consider using the LayoutBuilder widget to dynamically change a subtree during layout.'
        )
      );
    });

    test('marking a sibling dirty in performLayout', () {
      late RenderBox child1, child2;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(child2 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
          'A RenderObject must not mutate another RenderObject from a different render subtree in its performLayout method.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${child2.toStringShort()}\n'
          'Their common ancestor was:\n'
          '${block.toStringShort()}\n'
          'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
        )
      );
    });

    test('marking a descendant dirty in performLayout', () {
      late RenderBox child1;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(RenderLayoutTestBox(child1.markNeedsLayout));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderFlex.performLayout.\n'
          'A RenderObject must not mutate its descendants in its performLayout method.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'The ancestor RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${block.toStringShort()}\n'
          'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
        ),
      );
    });

    test('marking an out-of-band mutation in performLayout', () {
      late RenderProxyBox child1, child11, child2, child21;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(child2 = RenderLayoutTestBox(() {}));
      child1.child = child11 = RenderLayoutTestBox(() {});
      layout(block);

      expect(block.debugNeedsLayout, false);
      expect(child1.debugNeedsLayout, false);
      expect(child11.debugNeedsLayout, false);
      expect(child2.debugNeedsLayout, false);

      // Add a new child to child2 which is a relayout boundary.
      child2.child = child21 = RenderLayoutTestBox(() {}, onPerformLayout: child11.markNeedsLayout);

      FlutterError? error;
      pumpFrame(onErrors: () {
        error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception as FlutterError;
      });

      expect(
        error?.message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
          'The RenderObject was mutated when none of its ancestors is actively performing layout.\n'
          'The RenderObject being mutated was:\n'
          '${child11.toStringShort()}\n'
          'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${child21.toStringShort()}'
        ),
      );
    });
  });
}
