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

import '../rendering/mock_canvas.dart';

const Color green = Color(0xFF00FF00);
const Color yellow = Color(0xFFFFFF00);

void main() {
  testWidgets('SlottedRenderObjectWidget test', (WidgetTester tester) async {
    await tester.pumpWidget(buildWidget(
      topLeft: Container(
        height: 100,
        width: 80,
        color: yellow,
        child: const Text('topLeft'),
      ),
      bottomRight: Container(
        height: 120,
        width: 110,
        color: green,
        child: const Text('bottomRight'),
      ),
    ));

    expect(find.text('topLeft'), findsOneWidget);
    expect(find.text('bottomRight'), findsOneWidget);
    expect(tester.getSize(find.byType(_Diagonal)), const Size(80 + 110, 100 + 120));
    expect(find.byType(_Diagonal), paints
      ..rect(
        rect: const Rect.fromLTWH(0, 0, 80, 100),
        color: yellow,
      )
      ..rect(
        rect: const Rect.fromLTWH(80, 100, 110, 120),
        color: green,
      )
    );

    await tester.pumpWidget(buildWidget(
      topLeft: Container(
        height: 200,
        width: 100,
        color: yellow,
        child: const Text('topLeft'),
      ),
      bottomRight: Container(
        height: 220,
        width: 210,
        color: green,
        child: const Text('bottomRight'),
      ),
    ));

    expect(find.text('topLeft'), findsOneWidget);
    expect(find.text('bottomRight'), findsOneWidget);
    expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 210, 200 + 220));
    expect(find.byType(_Diagonal), paints
      ..rect(
        rect: const Rect.fromLTWH(0, 0, 100, 200),
        color: yellow,
      )
      ..rect(
        rect: const Rect.fromLTWH(100, 200, 210, 220),
        color: green,
      )
    );

    await tester.pumpWidget(buildWidget(
      topLeft: Container(
        height: 200,
        width: 100,
        color: yellow,
        child: const Text('topLeft'),
      ),
      bottomRight: Container(
        key: UniqueKey(),
        height: 230,
        width: 220,
        color: green,
        child: const Text('bottomRight'),
      ),
    ));

    expect(find.text('topLeft'), findsOneWidget);
    expect(find.text('bottomRight'), findsOneWidget);
    expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 220, 200 + 230));
    expect(find.byType(_Diagonal), paints
      ..rect(
        rect: const Rect.fromLTWH(0, 0, 100, 200),
        color: yellow,
      )
      ..rect(
        rect: const Rect.fromLTWH(100, 200, 220, 230),
        color: green,
      )
    );

    await tester.pumpWidget(buildWidget(
      topLeft: Container(
        height: 200,
        width: 100,
        color: yellow,
        child: const Text('topLeft'),
      ),
    ));

    expect(find.text('topLeft'), findsOneWidget);
    expect(find.text('bottomRight'), findsNothing);
    expect(tester.getSize(find.byType(_Diagonal)), const Size(100, 200));
    expect(find.byType(_Diagonal), paints
      ..rect(
        rect: const Rect.fromLTWH(0, 0, 100, 200),
        color: yellow,
      )
    );

    await tester.pumpWidget(buildWidget());
    expect(find.text('topLeft'), findsNothing);
    expect(find.text('bottomRight'), findsNothing);
    expect(tester.getSize(find.byType(_Diagonal)), Size.zero);
    expect(find.byType(_Diagonal), paintsNothing);

    await tester.pumpWidget(Container());
    expect(find.byType(_Diagonal), findsNothing);
  });

  test('nameForSlot', () {
    expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.bottomRight), 'bottomRight');
    expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.topLeft), 'topLeft');
    final _Slot slot = _Slot();
    expect(_RenderTest().publicNameForSlot(slot), slot.toString());
  });

  testWidgets('key reparenting', (WidgetTester tester) async {
    const Widget widget1 = SizedBox(key: ValueKey<String>('smol'), height: 10, width: 10);
    const Widget widget2 = SizedBox(key: ValueKey<String>('big'), height: 100, width: 100);
    const Widget nullWidget = SizedBox(key: ValueKey<String>('null'), height: 50, width: 50);

    await tester.pumpWidget(buildWidget(topLeft: widget1, bottomRight: widget2, nullSlot: nullWidget));
    final _RenderDiagonal renderObject = tester.renderObject(find.byType(_Diagonal));
    expect(renderObject._topLeft!.size, const Size(10, 10));
    expect(renderObject._bottomRight!.size, const Size(100, 100));
    expect(renderObject._nullSlot!.size, const Size(50, 50));

    final Element widget1Element = tester.element(find.byWidget(widget1));
    final Element widget2Element = tester.element(find.byWidget(widget2));
    final Element nullWidgetElement = tester.element(find.byWidget(nullWidget));

    // Swapping 1 and 2.
    await tester.pumpWidget(buildWidget(topLeft: widget2, bottomRight: widget1, nullSlot: nullWidget));
    expect(renderObject._topLeft!.size, const Size(100, 100));
    expect(renderObject._bottomRight!.size, const Size(10, 10));
    expect(renderObject._nullSlot!.size, const Size(50, 50));
    expect(widget1Element, same(tester.element(find.byWidget(widget1))));
    expect(widget2Element, same(tester.element(find.byWidget(widget2))));
    expect(nullWidgetElement, same(tester.element(find.byWidget(nullWidget))));

    // Shifting slots
    await tester.pumpWidget(buildWidget(topLeft: nullWidget, bottomRight: widget2, nullSlot: widget1));
    expect(renderObject._topLeft!.size, const Size(50, 50));
    expect(renderObject._bottomRight!.size, const Size(100, 100));
    expect(renderObject._nullSlot!.size, const Size(10, 10));
    expect(widget1Element, same(tester.element(find.byWidget(widget1))));
    expect(widget2Element, same(tester.element(find.byWidget(widget2))));
    expect(nullWidgetElement, same(tester.element(find.byWidget(nullWidget))));

    // Moving + Deleting.
    await tester.pumpWidget(buildWidget(bottomRight: widget2));
    expect(renderObject._topLeft, null);
    expect(renderObject._bottomRight!.size, const Size(100, 100));
    expect(renderObject._nullSlot, null);
    expect(widget1Element.debugIsDefunct, isTrue);
    expect(nullWidgetElement.debugIsDefunct, isTrue);
    expect(widget2Element, same(tester.element(find.byWidget(widget2))));

    // Moving.
    await tester.pumpWidget(buildWidget(topLeft: widget2));
    expect(renderObject._topLeft!.size, const Size(100, 100));
    expect(renderObject._bottomRight, null);
    expect(widget2Element, same(tester.element(find.byWidget(widget2))));
  });

  testWidgets('duplicated key error message', (WidgetTester tester) async {
    const Widget widget1 = SizedBox(key: ValueKey<String>('widget 1'), height: 10, width: 10);
    const Widget widget2 = SizedBox(key: ValueKey<String>('widget 1'), height: 100, width: 100);
    const Widget widget3 = SizedBox(key: ValueKey<String>('widget 1'), height: 50, width: 50);

    await tester.pumpWidget(buildWidget(topLeft: widget1, bottomRight: widget2, nullSlot: widget3));

    expect((tester.takeException() as FlutterError).toString(), equalsIgnoringHashCodes(
     'Multiple widgets used the same key in _Diagonal.\n'
     "The key [<'widget 1'>] was used by multiple widgets. The offending widgets were:\n"
     "  - SizedBox-[<'widget 1'>](width: 50.0, height: 50.0, renderObject: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT)\n"
     "  - SizedBox-[<'widget 1'>](width: 10.0, height: 10.0, renderObject: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT)\n"
     "  - SizedBox-[<'widget 1'>](width: 100.0, height: 100.0, renderObject: RenderConstrainedBox#a4685 NEEDS-LAYOUT NEEDS-PAINT)\n"
     'A key can only be specified on one widget at a time in the same parent widget.'
    ));
  });

  testWidgets('debugDescribeChildren', (WidgetTester tester) async {
    await tester.pumpWidget(buildWidget(
      topLeft: const SizedBox(
        height: 100,
        width: 80,
      ),
      bottomRight: const SizedBox(
        height: 120,
        width: 110,
      ),
    ));

    expect(
      tester.renderObject(find.byType(_Diagonal)).toStringDeep(),
      equalsIgnoringHashCodes(
        '_RenderDiagonal#00000 relayoutBoundary=up1\n'
        ' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
        ' │   _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
        ' │   TestWindow#00000] ← [root]\n'
        ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
        ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
        ' │ size: Size(190.0, 220.0)\n'
        ' │\n'
        ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
        ' │   creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
        ' │     MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
        ' │     View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
        ' │   parentData: offset=Offset(0.0, 0.0) (can use size)\n'
        ' │   constraints: BoxConstraints(unconstrained)\n'
        ' │   size: Size(80.0, 100.0)\n'
        ' │   additionalConstraints: BoxConstraints(w=80.0, h=100.0)\n'
        ' │\n'
        ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
        '     creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
        '       MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
        '       View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
        '     parentData: offset=Offset(80.0, 100.0) (can use size)\n'
        '     constraints: BoxConstraints(unconstrained)\n'
        '     size: Size(110.0, 120.0)\n'
        '     additionalConstraints: BoxConstraints(w=110.0, h=120.0)\n',
      )
    );
  });
}

Widget buildWidget({Widget? topLeft, Widget? bottomRight, Widget? nullSlot}) {
  return Directionality(
    textDirection: TextDirection.ltr,
    child: Align(
      alignment: Alignment.topLeft,
      child: _Diagonal(
        topLeft: topLeft,
        bottomRight: bottomRight,
        nullSlot: nullSlot,
      ),
    ),
  );
}

enum _DiagonalSlot {
  topLeft,
  bottomRight,
}

class _Diagonal extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_DiagonalSlot?> {
  const _Diagonal({
    this.topLeft,
    this.bottomRight,
    this.nullSlot,
  });

  final Widget? topLeft;
  final Widget? bottomRight;
  final Widget? nullSlot;

  @override
  Iterable<_DiagonalSlot?> get slots => <_DiagonalSlot?>[null, ..._DiagonalSlot.values];

  @override
  Widget? childForSlot(_DiagonalSlot? slot) {
    switch (slot) {
      case null:
        return nullSlot;
      case _DiagonalSlot.topLeft:
        return topLeft;
      case _DiagonalSlot.bottomRight:
        return bottomRight;
    }
  }

  @override
  SlottedContainerRenderObjectMixin<_DiagonalSlot?> createRenderObject(
    BuildContext context,
  ) {
    return _RenderDiagonal();
  }
}

class _RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin<_DiagonalSlot?> {
  RenderBox? get _topLeft => childForSlot(_DiagonalSlot.topLeft);
  RenderBox? get _bottomRight => childForSlot(_DiagonalSlot.bottomRight);
  RenderBox? get _nullSlot => childForSlot(null);

  @override
  void performLayout() {
    const BoxConstraints childConstraints = BoxConstraints();

    Size topLeftSize = Size.zero;
    if (_topLeft != null) {
      _topLeft!.layout(childConstraints, parentUsesSize: true);
      _positionChild(_topLeft!, Offset.zero);
      topLeftSize = _topLeft!.size;
    }

    Size bottomRightSize = Size.zero;
    if (_bottomRight != null) {
      _bottomRight!.layout(childConstraints, parentUsesSize: true);
      _positionChild(
        _bottomRight!,
        Offset(topLeftSize.width, topLeftSize.height),
      );
      bottomRightSize = _bottomRight!.size;
    }

    Size nullSlotSize = Size.zero;
    final RenderBox? nullSlot = _nullSlot;
    if (nullSlot != null) {
      nullSlot.layout(childConstraints, parentUsesSize: true);
      _positionChild(nullSlot, Offset.zero);
      nullSlotSize = nullSlot.size;
    }

    size = constraints.constrain(Size(
      topLeftSize.width + bottomRightSize.width + nullSlotSize.width,
      topLeftSize.height + bottomRightSize.height + nullSlotSize.height,
    ));
  }

  void _positionChild(RenderBox child, Offset offset) {
    (child.parentData! as BoxParentData).offset = offset;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (_topLeft != null) {
      _paintChild(_topLeft!, context, offset);
    }
    if (_bottomRight != null) {
      _paintChild(_bottomRight!, context, offset);
    }
  }

  void _paintChild(RenderBox child, PaintingContext context, Offset offset) {
    final BoxParentData childParentData = child.parentData! as BoxParentData;
    context.paintChild(child, childParentData.offset + offset);
  }

  String publicNameForSlot(_DiagonalSlot slot) => debugNameForSlot(slot);
}

class _Slot {
  @override
  String toString() => describeIdentity(this);
}

class _RenderTest extends RenderBox with SlottedContainerRenderObjectMixin<_Slot> {
  String publicNameForSlot(_Slot slot) => debugNameForSlot(slot);
}
