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

import 'rendering_tester.dart';

void main() {
  TestRenderingFlutterBinding.ensureInitialized();

  // This test has to be kept separate from object_test.dart because the way
  // the rendering_test.dart dependency of this test uses the bindings in not
  // compatible with existing tests in object_test.dart.
  test('reentrant paint error', () {
    late FlutterErrorDetails errorDetails;
    final RenderBox root = TestReentrantPaintingErrorRenderBox();
    layout(root, onErrors: () {
      errorDetails = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!;
    });
    pumpFrame(phase: EnginePhase.paint);

    expect(errorDetails, isNotNull);
    expect(errorDetails.stack, isNotNull);
    // Check the ErrorDetails without the stack trace
    final List<String> lines =  errorDetails.toString().split('\n');
    // The lines in the middle of the error message contain the stack trace
    // which will change depending on where the test is run.
    expect(lines.length, greaterThan(12));
    expect(
      lines.take(12).join('\n'),
      equalsIgnoringHashCodes(
        '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
        'The following assertion was thrown during paint():\n'
        'Tried to paint a RenderObject reentrantly.\n'
        'The following RenderObject was already being painted when it was painted again:\n'
        '  TestReentrantPaintingErrorRenderBox#00000:\n'
        '  parentData: <none>\n'
        '  constraints: BoxConstraints(w=800.0, h=600.0)\n'
        '  size: Size(100.0, 100.0)\n'
        'Since this typically indicates an infinite recursion, it is\n'
        'disallowed.\n'
        '\n'
        'When the exception was thrown, this was the stack:',
      ),
    );

    expect(
      lines.getRange(lines.length - 8, lines.length).join('\n'),
      equalsIgnoringHashCodes(
        'The following RenderObject was being processed when the exception was fired:\n'
        '  TestReentrantPaintingErrorRenderBox#00000:\n'
        '  parentData: <none>\n'
        '  constraints: BoxConstraints(w=800.0, h=600.0)\n'
        '  size: Size(100.0, 100.0)\n'
        'This RenderObject has no descendants.\n'
        '═════════════════════════════════════════════════════════════════\n',
      ),
    );
  });

  test('needsCompositingBitsUpdate paint error', () {
    late FlutterError flutterError;
    final RenderBox root = RenderRepaintBoundary(child: RenderSizedBox(const Size(100, 100)));
    try {
      layout(root);
      PaintingContext.repaintCompositedChild(root, debugAlsoPaintedParent: true);
    } on FlutterError catch (exception) {
      flutterError = exception;
    }

    expect(flutterError, isNotNull);
    // The lines in the middle of the error message contain the stack trace
    // which will change depending on where the test is run.
    expect(
      flutterError.toStringDeep(),
      equalsIgnoringHashCodes(
        'FlutterError\n'
        '   Tried to paint a RenderObject before its compositing bits were\n'
        '   updated.\n'
        '   The following RenderObject was marked as having dirty compositing bits at the time that it was painted:\n'
        '     RenderRepaintBoundary#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n'
        '     needs compositing\n'
        '     parentData: <none>\n'
        '     constraints: BoxConstraints(w=800.0, h=600.0)\n'
        '     layer: OffsetLayer#00000 DETACHED\n'
        '     size: Size(800.0, 600.0)\n'
        '     metrics: 0.0% useful (1 bad vs 0 good)\n'
        '     diagnosis: insufficient data to draw conclusion (less than five\n'
        '       repaints)\n'
        '   A RenderObject that still has dirty compositing bits cannot be\n'
        '   painted because this indicates that the tree has not yet been\n'
        '   properly configured for creating the layer tree.\n'
        '   This usually indicates an error in the Flutter framework itself.\n',
      ),
    );
    expect(
      flutterError.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
      'This usually indicates an error in the Flutter framework itself.',
    );
  });
}

class TestReentrantPaintingErrorRenderBox extends RenderBox {
  @override
  void paint(PaintingContext context, Offset offset) {
    // Cause a reentrant painting bug that would show up as a stack overflow if
    // it was not for debugging checks in RenderObject.
    context.paintChild(this, offset);
  }

  @override
  void performLayout() {
    size = const Size(100, 100);
  }
}
