// 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.

// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])

import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('RasterWidget can rasterize child', (WidgetTester tester) async {
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    final Key key = UniqueKey();
    await tester.pumpWidget(RepaintBoundary(
      key: key,
      child: Center(
        child: RasterWidget(
          controller: controller,
          child: Container(
            width: 100,
            height: 100,
            color: const Color(0xFFAABB11),
          ),
        ),
      ),
    ));
    // Rasterization is not actually complete until a frame has been pumped through
    // the engine.
    await tester.pumpAndSettle();

    await expectLater(find.byKey(key), matchesGoldenFile('raster_widget.yellow.png'));
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RasterWidget is not a repaint boundary when rasterizing', (WidgetTester tester) async {
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    await tester.pumpWidget(RepaintBoundary(
      child: Center(
        child: RasterWidget(
          controller: controller,
          child: Container(
            width: 100,
            height: 100,
            color: const Color(0xFFAABB11),
          ),
        ),
      ),
    ));

    expect(tester.layers, hasLength(3));
    expect(tester.layers.last, isA<PictureLayer>());

    controller.rasterize = false;
    await tester.pump();

    expect(tester.layers, hasLength(3));
    expect(tester.layers.last, isA<PictureLayer>());
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RasterWidget repaints when RasterWidgetDelegate notifies listeners', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    await tester.pumpWidget(RepaintBoundary(
      child: Center(
        child: RasterWidget(
          delegate: delegate,
          controller: controller,
          child: Container(
            width: 100,
            height: 100,
            color: const Color(0xFFAABB11),
          ),
        ),
      ),
    ));

    expect(delegate.count, 1);
    delegate.notify();
    await tester.pump();

    expect(delegate.count, 2);

    // Remove widget and verify removal of listeners.
    await tester.pumpWidget(const SizedBox());

    delegate.notify();
    await tester.pump();

    expect(delegate.count, 2);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RasterWidget will recreate raster when controller calls clear', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    await tester.pumpWidget(RepaintBoundary(
      child: Center(
        child: RasterWidget(
          delegate: delegate,
          controller: controller,
          child: Container(
            width: 100,
            height: 100,
            color: const Color(0xFFAABB11),
          ),
        ),
      ),
    ));

    expect(delegate.lastImage, isNotNull);
    final ui.Image lastImage = delegate.lastImage!;

    await tester.pump();

    // Raster is re-used
    expect(lastImage, equals(delegate.lastImage));

    controller.clear();

    await tester.pump();
    // Raster is re-created.
    expect(delegate.lastImage, isNotNull);
    expect(lastImage, isNot(delegate.lastImage));
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RasterWidget can update the delegate', (WidgetTester tester) async {
    final TestDelegate delegateA = TestDelegate();
    final TestDelegate delegateB = TestDelegate()
      ..shouldRepaintValue = true;
    TestDelegate delegate = delegateA;

    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    late void Function(void Function()) setStateFn;
    await tester.pumpWidget(StatefulBuilder(
      builder: (BuildContext context, void Function(void Function()) setState) {
        setStateFn = setState;
        return Center(
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            child: Container(
              width: 100,
              height: 100,
              color: const Color(0xFFAABB11),
            ),
          ),
        );
      })
    );

    expect(delegateA.count, 1);
    expect(delegateB.count, 0);
    setStateFn(() {
      delegate = delegateB;
    });
    await tester.pump();

    expect(delegateA.count, 1);
    expect(delegateB.count, 1);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RasterWidget can update the ValueNotifier', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controllerA = RasterWidgetController(rasterize: true);
    final RasterWidgetController controllerB = RasterWidgetController();
    RasterWidgetController controller = controllerA;
    late void Function(void Function()) setStateFn;
    await tester.pumpWidget(StatefulBuilder(
      builder: (BuildContext context, void Function(void Function()) setState) {
        setStateFn = setState;
        return Center(
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            child: Container(
              width: 100,
              height: 100,
              color: const Color(0xFFAABB11),
            ),
          ),
        );
      })
    );

    expect(delegate.count, 1);
    expect(tester.layers.last, isA<OffsetLayer>());
    setStateFn(() {
      controller = controllerB;
    });
    await tester.pump();

    expect(delegate.count, 1);
    expect(tester.layers.last, isA<PictureLayer>());

    // changes to old notifier do not impact widget.
    controllerA.rasterize = false;
    await tester.pump();

    expect(delegate.count, 1);
    expect(tester.layers.last, isA<PictureLayer>());

    await tester.pumpWidget(const SizedBox());

    // changes to notifier do not impact widget after disposal.
    controllerB.rasterize = true;
    await tester.pump();
    expect(delegate.count, 1);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget correctly attaches and detaches delegate callbacks', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    final RenderRasterWidget rasterWidget = RenderRasterWidget(
      delegate: delegate,
      controller: controller,
      devicePixelRatio: 1.0,
      mode: RasterizeMode.enabled,
    );

    expect(delegate.addedListenerCount, 0);
    expect(delegate.removedListenerCount, 0);

    final PipelineOwner owner = PipelineOwner();
    rasterWidget.attach(owner);

    expect(delegate.addedListenerCount, 1);
    expect(delegate.removedListenerCount, 0);

    rasterWidget.detach();

    expect(delegate.addedListenerCount, 1);
    expect(delegate.removedListenerCount, 1);

    final TestDelegate updatedDelegate = TestDelegate();
    rasterWidget.delegate = updatedDelegate;

    // No listeners added or removed while not attached.
    expect(updatedDelegate.addedListenerCount, 0);
    expect(updatedDelegate.removedListenerCount, 0);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget correctly attaches and detaches controller callbacks', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final TestController controller = TestController();
    final RenderRasterWidget rasterWidget = RenderRasterWidget(
      delegate: delegate,
      controller: controller,
      devicePixelRatio: 1.0,
      mode: RasterizeMode.enabled,
    );

    expect(controller.addedListenerCount, 0);
    expect(controller.removedListenerCount, 0);

    final PipelineOwner owner = PipelineOwner();
    rasterWidget.attach(owner);

    expect(controller.addedListenerCount, 1);
    expect(controller.removedListenerCount, 0);

    rasterWidget.detach();

    expect(controller.addedListenerCount, 1);
    expect(controller.removedListenerCount, 1);

    final TestController updatedController = TestController();
    rasterWidget.controller = updatedController;

    // No listeners added or removed while not attached.
    expect(updatedController.addedListenerCount, 0);
    expect(updatedController.removedListenerCount, 0);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget does not error on rasterization of child with empty size', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);

    await tester.pumpWidget(
      Center(
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            child: const SizedBox(),
          ),
        ),
      ),
    );
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689


  testWidgets('RenderRasterWidget throws assertion if platform view is encountered', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);

    await tester.pumpWidget(
      Center(
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            child: const SizedBox(
              width: 100,
              height: 100,
              child: TestPlatformView(),
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isA<FlutterError>()
      .having((FlutterError error) => error.message, 'message', contains('RasterWidget used with a child that contains a PlatformView')));
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget does not assert if RasterizeMode.forced', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);

    await tester.pumpWidget(
      Center(
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            mode: RasterizeMode.forced,
            child: const SizedBox(
              width: 100,
              height: 100,
              child: TestPlatformView(),
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isNull);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget fallbacks to delegate if PlatformView is present', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    final TestFallback fallback = TestFallback();
    await tester.pumpWidget(
      Center(
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            fallback: fallback,
            child: const SizedBox(
              width: 100,
              height: 100,
              child: TestPlatformView(),
            ),
          ),
        ),
      ),
    );

    expect(fallback.calledFallback, 1);
    expect(delegate.count, 0);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689

  testWidgets('RenderRasterWidget fallbacks to delegate if mode: RasterizeMode.fallback', (WidgetTester tester) async {
    final TestDelegate delegate = TestDelegate();
    final RasterWidgetController controller = RasterWidgetController(rasterize: true);
    final TestFallback fallback = TestFallback();
    await tester.pumpWidget(
      Center(
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: RasterWidget(
            delegate: delegate,
            controller: controller,
            fallback: fallback,
            mode: RasterizeMode.fallback,
            child: const SizedBox(
              width: 100,
              height: 100,
            ),
          ),
        ),
      ),
    );

    expect(fallback.calledFallback, 1);
    expect(delegate.count, 0);
  }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
}

class TestFallback extends RasterWidgetFallbackDelegate {
  int calledFallback = 0;

  @override
  void paintFallback(PaintingContext context, ui.Offset offset, ui.Size size, PaintingContextCallback painter) {
    calledFallback += 1;
  }

}

class TestController extends RasterWidgetController {
  int addedListenerCount = 0;
  int removedListenerCount = 0;

  @override
  void addListener(ui.VoidCallback listener) {
    addedListenerCount += 1;
    super.addListener(listener);
  }

  @override
  void removeListener(ui.VoidCallback listener) {
    removedListenerCount += 1;
    super.removeListener(listener);
  }
}

class TestDelegate extends RasterWidgetDelegate {
  int count = 0;
  bool shouldRepaintValue = false;
  ui.Image? lastImage;

  int addedListenerCount = 0;
  int removedListenerCount = 0;

  @override
  void addListener(ui.VoidCallback listener) {
    addedListenerCount += 1;
    super.addListener(listener);
  }

  @override
  void removeListener(ui.VoidCallback listener) {
    removedListenerCount += 1;
    super.removeListener(listener);
  }

  void notify() {
    notifyListeners();
  }

  @override
  void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
    count += 1;
    lastImage = image;
  }

  @override
  bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate) => shouldRepaintValue;
}


class TestPlatformView extends SingleChildRenderObjectWidget {
  const TestPlatformView({super.key});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderTestPlatformView();
  }
}

class RenderTestPlatformView extends RenderProxyBox {

  @override
  void paint(PaintingContext context, ui.Offset offset) {
    context.addLayer(PlatformViewLayer(rect: offset & size, viewId: 1));
  }
}
