// 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('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(r'''
_RenderDiagonal#00000 relayoutBoundary=up1
 │ creator: _Diagonal ← Align ← Directionality ← [root]
 │ parentData: offset=Offset(0.0, 0.0) (can use size)
 │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)
 │ size: Size(190.0, 220.0)
 │
 ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2
 │   creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root]
 │   parentData: offset=Offset(0.0, 0.0) (can use size)
 │   constraints: BoxConstraints(unconstrained)
 │   size: Size(80.0, 100.0)
 │   additionalConstraints: BoxConstraints(w=80.0, h=100.0)
 │
 └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2
     creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root]
     parentData: offset=Offset(80.0, 100.0) (can use size)
     constraints: BoxConstraints(unconstrained)
     size: Size(110.0, 120.0)
     additionalConstraints: BoxConstraints(w=110.0, h=120.0)
''')
    );
  });
}

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

enum _DiagonalSlot {
  topLeft,
  bottomRight,
}

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

  final Widget? topLeft;
  final Widget? bottomRight;

  @override
  Iterable<_DiagonalSlot> get slots => _DiagonalSlot.values;

  @override
  Widget? childForSlot(_DiagonalSlot slot) {
    switch (slot) {
      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);

  @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 = constraints.constrain(Size(
      topLeftSize.width + bottomRightSize.width,
      topLeftSize.height + bottomRightSize.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);
}