// Copyright 2013 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.
//
// Copyright 2026 Kerem Bas (palette_generator_plus fork additions).
// Ported from the original palette_generator test suite to verify drop-in
// parity of the LegacyQuantizer.

import 'dart:ui' as ui show Codec, FrameInfo, Image, instantiateImageCodec;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:palette_generator_plus/palette_generator_plus.dart';

import 'encoded_images.dart';

/// An image provider implementation for testing that takes a pre-loaded image.
/// This avoids handling asynchronous I/O in the test zone, which is
/// problematic.
class FakeImageProvider extends ImageProvider<FakeImageProvider> {
  const FakeImageProvider(this._image, {this.scale = 1.0});

  final ui.Image _image;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  @override
  Future<FakeImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<FakeImageProvider>(this);
  }

  @override
  ImageStreamCompleter loadImage(
    FakeImageProvider key,
    ImageDecoderCallback decode,
  ) {
    assert(key == this);
    return OneFrameImageStreamCompleter(
      SynchronousFuture<ImageInfo>(
        ImageInfo(image: _image, scale: scale),
      ),
    );
  }
}

Future<FakeImageProvider> loadImage(Uint8List data) async {
  final ui.Codec codec = await ui.instantiateImageCodec(data);
  final ui.FrameInfo frameInfo = await codec.getNextFrame();
  return FakeImageProvider(frameInfo.image);
}

Future<void> main() async {
  // Use the LegacyQuantizer throughout this suite so we can assert the exact
  // median-cut output of the original palette_generator package.
  const Quantizer legacy = LegacyQuantizer();

  // Load the images outside of the test zone so that IO doesn't get
  // complicated.
  final Map<String, FakeImageProvider> testImages = <String, FakeImageProvider>{
    'tall_blue': await loadImage(kImageDataTallBlue),
    'wide_red': await loadImage(kImageDataWideRed),
    'dominant': await loadImage(kImageDataDominant),
    'landscape': await loadImage(kImageDataLandscape),
  };

  testWidgets('Initialize the image cache', (WidgetTester tester) async {
    // We need to have a testWidgets test in order to initialize the image
    // cache for the other tests, but they timeout if they too are testWidgets
    // tests.
    await tester.pumpWidget(const Placeholder());
  });

  test(
      "PaletteGenerator.fromByteData throws when the size doesn't match the byte data size",
      () {
    expect(
      () async {
        final ByteData? data =
            await testImages['tall_blue']!._image.toByteData();
        await PaletteGenerator.fromByteData(
          EncodedImage(
            data!,
            width: 1,
            height: 1,
          ),
        );
      },
      throwsAssertionError,
    );
  });

  test('PaletteGenerator.fromImage works', () async {
    final PaletteGenerator palette = await PaletteGenerator.fromImage(
        testImages['tall_blue']!._image,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(1));
    expect(palette.paletteColors[0].color,
        within<Color>(distance: 8, from: const Color(0xff0000ff)));
  });

  test('PaletteGenerator works on 1-pixel tall blue image', () async {
    final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        testImages['tall_blue']!,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(1));
    expect(palette.paletteColors[0].color,
        within<Color>(distance: 8, from: const Color(0xff0000ff)));
  });

  test('PaletteGenerator works on 1-pixel wide red image', () async {
    final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        testImages['wide_red']!,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(1));
    expect(palette.paletteColors[0].color,
        within<Color>(distance: 8, from: const Color(0xffff0000)));
  });

  test('PaletteGenerator finds dominant color and text colors', () async {
    final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        testImages['dominant']!,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(3));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff0000ff)));
    expect(palette.dominantColor!.titleTextColor,
        within<Color>(distance: 8, from: const Color(0x8affffff)));
    expect(palette.dominantColor!.bodyTextColor,
        within<Color>(distance: 8, from: const Color(0xb2ffffff)));
  });

  test('PaletteGenerator works with regions', () async {
    final ImageProvider imageProvider = testImages['dominant']!;
    Rect region = const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0);
    const Size size = Size(100.0, 100.0);
    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        imageProvider,
        region: region,
        size: size,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(3));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff0000ff)));

    region = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        region: region, size: size, quantizer: legacy);
    expect(palette.paletteColors.length, equals(1));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xffff0000)));

    region = const Rect.fromLTRB(0.0, 0.0, 30.0, 20.0);
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        region: region, size: size, quantizer: legacy);
    expect(palette.paletteColors.length, equals(3));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff00ff00)));
  });

  test('PaletteGenerator works as expected on a real image', () async {
    final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        testImages['landscape']!,
        quantizer: legacy);
    final List<PaletteColor> expectedSwatches = <PaletteColor>[
      PaletteColor(const Color(0xff3f630c), 10137),
      PaletteColor(const Color(0xff3c4b2a), 4773),
      PaletteColor(const Color(0xff81b2e9), 4762),
      PaletteColor(const Color(0xffc0d6ec), 4714),
      PaletteColor(const Color(0xff4c4f50), 2465),
      PaletteColor(const Color(0xff5c635b), 2463),
      PaletteColor(const Color(0xff6e80a2), 2421),
      PaletteColor(const Color(0xff9995a3), 1214),
      PaletteColor(const Color(0xff676c4d), 1213),
      PaletteColor(const Color(0xffc4b2b2), 1173),
      PaletteColor(const Color(0xff445166), 1040),
      PaletteColor(const Color(0xff475d83), 1019),
      PaletteColor(const Color(0xff7e7360), 589),
      PaletteColor(const Color(0xfff6b835), 286),
      PaletteColor(const Color(0xffb9983d), 152),
      PaletteColor(const Color(0xffe3ab35), 149),
    ];
    final Iterable<Color> expectedColors =
        expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
    expect(palette.paletteColors, containsAll(expectedSwatches));
    expect(palette.vibrantColor, isNotNull);
    expect(palette.lightVibrantColor, isNotNull);
    expect(palette.darkVibrantColor, isNotNull);
    expect(palette.mutedColor, isNotNull);
    expect(palette.lightMutedColor, isNotNull);
    expect(palette.darkMutedColor, isNotNull);
    expect(palette.vibrantColor!.color,
        within<Color>(distance: 8, from: const Color(0xfff6b835)));
    expect(palette.lightVibrantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff82b2e9)));
    expect(palette.darkVibrantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff3f630c)));
    expect(palette.mutedColor!.color,
        within<Color>(distance: 8, from: const Color(0xff6c7fa2)));
    expect(palette.lightMutedColor!.color,
        within<Color>(distance: 8, from: const Color(0xffc4b2b2)));
    expect(palette.darkMutedColor!.color,
        within<Color>(distance: 8, from: const Color(0xff3c4b2a)));
    expect(palette.colors, containsAllInOrder(expectedColors));
    expect(palette.colors.length, equals(palette.paletteColors.length));
  });

  test('PaletteGenerator limits max colors', () async {
    final ImageProvider imageProvider = testImages['landscape']!;
    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        imageProvider,
        maximumColorCount: 32,
        quantizer: legacy);
    expect(palette.paletteColors.length, equals(31));
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        maximumColorCount: 1, quantizer: legacy);
    expect(palette.paletteColors.length, equals(1));
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        maximumColorCount: 15, quantizer: legacy);
    expect(palette.paletteColors.length, equals(15));
  });

  test('PaletteGenerator filters work', () async {
    final ImageProvider imageProvider = testImages['landscape']!;
    // First, test that supplying the default filter is the same as not
    // supplying one.
    List<PaletteFilter> filters = <PaletteFilter>[
      avoidRedBlackWhitePaletteFilter
    ];
    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        imageProvider,
        filters: filters,
        quantizer: legacy);
    final List<PaletteColor> expectedSwatches = <PaletteColor>[
      PaletteColor(const Color(0xff3f630c), 10137),
      PaletteColor(const Color(0xff3c4b2a), 4773),
      PaletteColor(const Color(0xff81b2e9), 4762),
      PaletteColor(const Color(0xffc0d6ec), 4714),
      PaletteColor(const Color(0xff4c4f50), 2465),
      PaletteColor(const Color(0xff5c635b), 2463),
      PaletteColor(const Color(0xff6e80a2), 2421),
      PaletteColor(const Color(0xff9995a3), 1214),
      PaletteColor(const Color(0xff676c4d), 1213),
      PaletteColor(const Color(0xffc4b2b2), 1173),
      PaletteColor(const Color(0xff445166), 1040),
      PaletteColor(const Color(0xff475d83), 1019),
      PaletteColor(const Color(0xff7e7360), 589),
      PaletteColor(const Color(0xfff6b835), 286),
      PaletteColor(const Color(0xffb9983d), 152),
      PaletteColor(const Color(0xffe3ab35), 149),
    ];
    final Iterable<Color> expectedColors =
        expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
    expect(palette.paletteColors, containsAll(expectedSwatches));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff3f630c)));
    expect(palette.colors, containsAllInOrder(expectedColors));

    // A non-default filter works (and the default filter isn't applied too).
    filters = <PaletteFilter>[onlyBluePaletteFilter];
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        filters: filters, quantizer: legacy);
    final List<PaletteColor> blueSwatches = <PaletteColor>[
      PaletteColor(const Color(0xff4c5c75), 1515),
      PaletteColor(const Color(0xff7483a1), 1505),
      PaletteColor(const Color(0xff515661), 1476),
      PaletteColor(const Color(0xff769dd4), 1470),
      PaletteColor(const Color(0xff3e4858), 777),
      PaletteColor(const Color(0xff98a3bc), 760),
      PaletteColor(const Color(0xffb4c7e0), 760),
      PaletteColor(const Color(0xff99bbe5), 742),
      PaletteColor(const Color(0xffcbdef0), 701),
      PaletteColor(const Color(0xff1c212b), 429),
      PaletteColor(const Color(0xff393c46), 417),
      PaletteColor(const Color(0xff526483), 394),
      PaletteColor(const Color(0xff61708b), 372),
      PaletteColor(const Color(0xff5e8ccc), 345),
      PaletteColor(const Color(0xff587ab4), 194),
      PaletteColor(const Color(0xff5584c8), 182),
    ];
    final Iterable<Color> expectedBlues =
        blueSwatches.map<Color>((PaletteColor swatch) => swatch.color);

    expect(palette.paletteColors, containsAll(blueSwatches));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xff4c5c75)));
    expect(palette.colors, containsAllInOrder(expectedBlues));

    // More than one filter is the intersection of the two filters.
    filters = <PaletteFilter>[onlyBluePaletteFilter, onlyCyanPaletteFilter];
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        filters: filters, quantizer: legacy);
    final List<PaletteColor> blueGreenSwatches = <PaletteColor>[
      PaletteColor(const Color(0xffc8e8f8), 87),
      PaletteColor(const Color(0xff5c6c74), 73),
      PaletteColor(const Color(0xff6f8088), 49),
      PaletteColor(const Color(0xff687880), 49),
      PaletteColor(const Color(0xff506068), 45),
      PaletteColor(const Color(0xff485860), 39),
      PaletteColor(const Color(0xff405058), 21),
      PaletteColor(const Color(0xffd6ebf3), 11),
      PaletteColor(const Color(0xff2f3f47), 7),
      PaletteColor(const Color(0xff0f1f27), 6),
      PaletteColor(const Color(0xffc0e0f0), 6),
      PaletteColor(const Color(0xff203038), 3),
      PaletteColor(const Color(0xff788890), 2),
      PaletteColor(const Color(0xff384850), 2),
      PaletteColor(const Color(0xff98a8b0), 1),
      PaletteColor(const Color(0xffa8b8c0), 1),
    ];
    final Iterable<Color> expectedBlueGreens =
        blueGreenSwatches.map<Color>((PaletteColor swatch) => swatch.color);

    expect(palette.paletteColors, containsAll(blueGreenSwatches));
    expect(palette.dominantColor, isNotNull);
    expect(palette.dominantColor!.color,
        within<Color>(distance: 8, from: const Color(0xffc8e8f8)));
    expect(palette.colors, containsAllInOrder(expectedBlueGreens));

    // Mutually exclusive filters return an empty palette.
    filters = <PaletteFilter>[onlyBluePaletteFilter, onlyGreenPaletteFilter];
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        filters: filters, quantizer: legacy);
    expect(palette.paletteColors, isEmpty);
    expect(palette.dominantColor, isNull);
    expect(palette.colors, isEmpty);
  });

  test('PaletteGenerator targets work', () async {
    final ImageProvider imageProvider = testImages['landscape']!;
    // Passing an empty set of targets works the same as passing a null targets
    // list.
    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
        imageProvider,
        targets: <PaletteTarget>[],
        quantizer: legacy);
    expect(palette.selectedSwatches, isNotEmpty);
    expect(palette.vibrantColor, isNotNull);
    expect(palette.lightVibrantColor, isNotNull);
    expect(palette.darkVibrantColor, isNotNull);
    expect(palette.mutedColor, isNotNull);
    expect(palette.lightMutedColor, isNotNull);
    expect(palette.darkMutedColor, isNotNull);

    // Passing targets augments the baseTargets, and those targets are found.
    final List<PaletteTarget> saturationExtremeTargets = <PaletteTarget>[
      PaletteTarget(minimumSaturation: 0.85),
      PaletteTarget(maximumSaturation: .25),
    ];
    palette = await PaletteGenerator.fromImageProvider(imageProvider,
        targets: saturationExtremeTargets, quantizer: legacy);
    expect(palette.vibrantColor, isNotNull);
    expect(palette.lightVibrantColor, isNotNull);
    expect(palette.darkVibrantColor, isNotNull);
    expect(palette.mutedColor, isNotNull);
    expect(palette.lightMutedColor, isNotNull);
    expect(palette.darkMutedColor, isNotNull);
    expect(palette.selectedSwatches.length,
        equals(PaletteTarget.baseTargets.length + 2));
    final PaletteColor? selectedSwatchesFirst =
        palette.selectedSwatches[saturationExtremeTargets[0]];
    final PaletteColor? selectedSwatchesSecond =
        palette.selectedSwatches[saturationExtremeTargets[1]];
    expect(selectedSwatchesFirst, isNotNull);
    expect(selectedSwatchesSecond, isNotNull);
    expect(selectedSwatchesFirst!.color, equals(const Color(0xfff6b835)));
    expect(selectedSwatchesSecond!.color, equals(const Color(0xff6e80a2)));
  });

  test('PaletteGenerator produces consistent results', () async {
    final ImageProvider imageProvider = testImages['landscape']!;

    PaletteGenerator lastPalette = await PaletteGenerator.fromImageProvider(
        imageProvider,
        quantizer: legacy);
    for (int i = 0; i < 5; ++i) {
      final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
          imageProvider,
          quantizer: legacy);
      expect(palette.paletteColors.length, lastPalette.paletteColors.length);
      expect(palette.vibrantColor, equals(lastPalette.vibrantColor));
      expect(palette.lightVibrantColor, equals(lastPalette.lightVibrantColor));
      expect(palette.darkVibrantColor, equals(lastPalette.darkVibrantColor));
      expect(palette.mutedColor, equals(lastPalette.mutedColor));
      expect(palette.lightMutedColor, equals(lastPalette.lightMutedColor));
      expect(palette.darkMutedColor, equals(lastPalette.darkMutedColor));
      expect(palette.dominantColor, isNotNull);
      expect(lastPalette.dominantColor, isNotNull);
      expect(palette.dominantColor!.color,
          within<Color>(distance: 8, from: lastPalette.dominantColor!.color));
      lastPalette = palette;
    }
  });

  test('PaletteGenerator.fromByteData works in non-root isolate', () async {
    final ui.Image image = testImages['tall_blue']!._image;
    final ByteData? data = await image.toByteData();
    final PaletteGenerator palette =
        await compute<EncodedImage, PaletteGenerator>(
      _computeFromByteData,
      EncodedImage(data!, width: image.width, height: image.height),
    );
    expect(palette.paletteColors.length, equals(1));
    expect(palette.paletteColors[0].color,
        within<Color>(distance: 8, from: const Color(0xff0000ff)));
  });

  test('PaletteColor == does not crash on invalid comparisons', () {
    final PaletteColor paletteColorA = PaletteColor(const Color(0xFFFFFFFF), 1);
    final PaletteColor paletteColorB = PaletteColor(const Color(0xFFFFFFFF), 1);
    final Object object = Object();

    expect(paletteColorA == paletteColorB, true);
    expect(paletteColorA == object, false);
  });

  test('PaletteTarget == does not crash on invalid comparisons', () {
    final PaletteTarget paletteTargetA = PaletteTarget();
    final PaletteTarget paletteTargetB = PaletteTarget();
    final Object object = Object();

    expect(paletteTargetA == paletteTargetB, true);
    expect(paletteTargetA == object, false);
  });
}

Future<PaletteGenerator> _computeFromByteData(EncodedImage encodedImage) async {
  return PaletteGenerator.fromByteData(encodedImage,
      quantizer: const LegacyQuantizer());
}

bool onlyBluePaletteFilter(HSLColor hslColor) {
  const double blueLineMinHue = 185.0;
  const double blueLineMaxHue = 260.0;
  const double blueLineMaxSaturation = 0.82;
  return hslColor.hue >= blueLineMinHue &&
      hslColor.hue <= blueLineMaxHue &&
      hslColor.saturation <= blueLineMaxSaturation;
}

bool onlyCyanPaletteFilter(HSLColor hslColor) {
  const double cyanLineMinHue = 165.0;
  const double cyanLineMaxHue = 200.0;
  const double cyanLineMaxSaturation = 0.82;
  return hslColor.hue >= cyanLineMinHue &&
      hslColor.hue <= cyanLineMaxHue &&
      hslColor.saturation <= cyanLineMaxSaturation;
}

bool onlyGreenPaletteFilter(HSLColor hslColor) {
  const double greenLineMinHue = 80.0;
  const double greenLineMaxHue = 165.0;
  const double greenLineMaxSaturation = 0.82;
  return hslColor.hue >= greenLineMinHue &&
      hslColor.hue <= greenLineMaxHue &&
      hslColor.saturation <= greenLineMaxSaturation;
}
