// 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 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

import 'utils/fake_and_mock_utils.dart';

void main() {
  group('TestDisplay', () {
    Display trueDisplay() => PlatformDispatcher.instance.displays.single;
    TestDisplay boundDisplay() => WidgetsBinding.instance.platformDispatcher.displays.single as TestDisplay;

    tearDown(() {
      boundDisplay().reset();
    });

    testWidgets('can handle new methods without breaking', (WidgetTester tester) async {
      final dynamic testDisplay = tester.view.display;
      //ignore: avoid_dynamic_calls
      expect(testDisplay.someNewProperty, null);
    });

    testWidgets('can fake devicePixelRatio', (WidgetTester tester) async {
      verifyPropertyFaked<double>(
        tester: tester,
        realValue: trueDisplay().devicePixelRatio,
        fakeValue: trueDisplay().devicePixelRatio + 1,
        propertyRetriever: () => boundDisplay().devicePixelRatio,
        propertyFaker: (_, double fake) {
          boundDisplay().devicePixelRatio = fake;
        },
      );
    });

    testWidgets('can reset devicePixelRatio', (WidgetTester tester) async {
      verifyPropertyReset<double>(
        tester: tester,
        fakeValue: trueDisplay().devicePixelRatio + 1,
        propertyRetriever: () => boundDisplay().devicePixelRatio,
        propertyResetter: () => boundDisplay().resetDevicePixelRatio(),
        propertyFaker: (double fake) {
          boundDisplay().devicePixelRatio = fake;
        },
      );
    });

    testWidgets('resetting devicePixelRatio also resets view.devicePixelRatio', (WidgetTester tester) async {
      verifyPropertyReset(
        tester: tester,
        fakeValue: trueDisplay().devicePixelRatio + 1,
        propertyRetriever: () => tester.view.devicePixelRatio,
        propertyResetter: () => boundDisplay().resetDevicePixelRatio(),
        propertyFaker: (double dpr) => boundDisplay().devicePixelRatio = dpr,
      );
    });

    testWidgets('updating devicePixelRatio also updates view.devicePixelRatio', (WidgetTester tester) async {
      tester.view.display.devicePixelRatio = tester.view.devicePixelRatio + 1;

      expect(tester.view.devicePixelRatio, tester.view.display.devicePixelRatio);
    });

    testWidgets('can fake refreshRate', (WidgetTester tester) async {
      verifyPropertyFaked<double>(
        tester: tester,
        realValue: trueDisplay().refreshRate,
        fakeValue: trueDisplay().refreshRate + 1,
        propertyRetriever: () => boundDisplay().refreshRate,
        propertyFaker: (_, double fake) {
          boundDisplay().refreshRate = fake;
        },
      );
    });

    testWidgets('can reset refreshRate', (WidgetTester tester) async {
      verifyPropertyReset<double>(
        tester: tester,
        fakeValue: trueDisplay().refreshRate + 1,
        propertyRetriever: () => boundDisplay().refreshRate,
        propertyResetter: () => boundDisplay().resetRefreshRate(),
        propertyFaker: (double fake) {
          boundDisplay().refreshRate = fake;
        },
      );
    });

    testWidgets('can fake size', (WidgetTester tester) async {
      verifyPropertyFaked<Size>(
        tester: tester,
        realValue: trueDisplay().size,
        fakeValue: const Size(354, 856),
        propertyRetriever: () => boundDisplay().size,
        propertyFaker: (_, Size fake) {
          boundDisplay().size = fake;
        },
      );
    });

    testWidgets('can reset size', (WidgetTester tester) async {
      verifyPropertyReset<Size>(
        tester: tester,
        fakeValue: const Size(465, 980),
        propertyRetriever: () => boundDisplay().size,
        propertyResetter: () => boundDisplay().resetSize(),
        propertyFaker: (Size fake) {
          boundDisplay().size = fake;
        },
      );
    });

    testWidgets('can reset all values', (WidgetTester tester) async {
      final DisplaySnapshot initial = DisplaySnapshot(tester.view.display);

      tester.view.display.devicePixelRatio = 7;
      tester.view.display.refreshRate = 40;
      tester.view.display.size = const Size(476, 823);

      final DisplaySnapshot faked = DisplaySnapshot(tester.view.display);

      tester.view.display.reset();

      final DisplaySnapshot reset = DisplaySnapshot(tester.view.display);

      expect(initial, isNot(matchesSnapshot(faked)));
      expect(initial, matchesSnapshot(reset));
    });
  });
}

class DisplaySnapshot {
  DisplaySnapshot(Display display) :
    devicePixelRatio = display.devicePixelRatio,
    refreshRate = display.refreshRate,
    id = display.id,
    size = display.size;

  final double devicePixelRatio;
  final double refreshRate;
  final int id;
  final Size size;
}

Matcher matchesSnapshot(DisplaySnapshot expected) => _DisplaySnapshotMatcher(expected);

class _DisplaySnapshotMatcher extends Matcher {
  _DisplaySnapshotMatcher(this.expected);

  final DisplaySnapshot expected;

  @override
  Description describe(Description description) {
    description.add('snapshot of a Display matches');
    return description;
  }

  @override
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
    assert(item is DisplaySnapshot, 'Can only match against snapshots of Display.');
    final DisplaySnapshot actual = item as DisplaySnapshot;

    if (actual.devicePixelRatio != expected.devicePixelRatio) {
      mismatchDescription.add('actual.devicePixelRatio (${actual.devicePixelRatio}) did not match expected.devicePixelRatio (${expected.devicePixelRatio})');
    }
    if (actual.refreshRate != expected.refreshRate) {
      mismatchDescription.add('actual.refreshRate (${actual.refreshRate}) did not match expected.refreshRate (${expected.refreshRate})');
    }
    if (actual.size != expected.size) {
      mismatchDescription.add('actual.size (${actual.size}) did not match expected.size (${expected.size})');
    }
    if (actual.id != expected.id) {
      mismatchDescription.add('actual.id (${actual.id}) did not match expected.id (${expected.id})');
    }

    return mismatchDescription;
  }

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
    assert(item is DisplaySnapshot, 'Can only match against snapshots of Display.');
    final DisplaySnapshot actual = item as DisplaySnapshot;

    return actual.devicePixelRatio == expected.devicePixelRatio &&
      actual.refreshRate == expected.refreshRate &&
      actual.size == expected.size &&
      actual.id == expected.id;
  }
}
