// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'package:test/test.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';

void main() {
  group('Option<T>', () {
    test('`isPresent` returns true on not null value', () {
      expect(
        const Value<int>(10).isPresent,
        true,
      );
      expect(
        const None<int>().isPresent,
        false,
      );
    });

    test('`isNull` returns true on null value', () {
      expect(
        const Value<int>(10).isNull,
        false,
      );
      expect(
        const None<int>().isNull,
        true,
      );
    });

    test('`get` returns value', () {
      const Option<int> x = Value(2);
      expect(x.get(), 2);

      const Option<int> y = None();
      expect(y.get(), null);
    });

    test('unwrap() returns value on Value', () {
      const Option<int> x = Value(2);
      expect(x.unwrap<int>(), 2);
      expect(() => x.unwrap<String>(), throwsA(isException));
    });

    test('unwrap() throws on None', () {
      const Option<int> x = None();
      expect(() => x.unwrap<int>(), throwsA(isException));
    });

    test('unwrapAsync() returns value on Value', () async {
      const Option<int> x = Value(2);
      expect(await x.unwrapAsync<int>(), 2);
      expect(() async => x.unwrapAsync<String>(), throwsA(isException));
    });

    test('unwrapAsync() throws on Nonce', () async {
      const Option<int> x = None();
      expect(() async => x.unwrapAsync<int>(), throwsA(isException));
    });

    test('ifPresent() invokes command on Value', () {
      const Value<int>(2).ifPresent(
        print,
      );
    });

    test('orElse() returns value', () {
      expect(
        const Value<int>(10).orElse(12),
        10,
      );

      expect(
        const None<int>().orElse(12),
        12,
      );
    });

    test('orElseLazy() returns value', () {
      expect(
        const Value<int>(10).orElseLazy(() => 12),
        10,
      );

      expect(
        const None<int>().orElseLazy(() => 12),
        12,
      );
    });

    test(
      'and() returns res if the option is `Value`, otherwise '
      'returns `None`.',
      () {
        const Option<int> x1 = Value(2);
        const Option<int> y1 = None();
        expect(x1.and(y1), y1);

        const Option<String> x2 = None();
        const Option<String> y2 = Value('foo');
        expect(x2.and(y2), x2);

        const Option<String> x3 = None();
        const Option<String> y3 = None();
        expect(x3.and(y3), x3);

        const Option<dynamic> x4 = Value(2);
        const Option<dynamic> y4 = Value('different Option type');
        expect(x4.and(y4), y4);

        const Option<int> x5 = Value(2);
        const Option<int> y5 = Value(5);
        expect(x5.and(y5), y5);
      },
    );

    test(
      'or() returns res if the option is `None`, otherwise '
      'returns the `Value`.',
      () {
        const Option<int> x1 = Value(2);
        const Option<int> y1 = None();
        expect(x1.or(y1), x1);

        const Option<String> x2 = None();
        const Option<String> y2 = Value('foo');
        expect(x2.or(y2), y2);

        const Option<void> x3 = None();
        const Option<void> y3 = None();
        expect(x3.or(y3), y3);

        const Option<dynamic> x4 = Value(2);
        const Option<dynamic> y4 = Value('different result type');
        expect(x4.or(y4), x4);

        const Option<int> x5 = Value(2);
        const Option<int> y5 = Value(5);
        expect(x5.or(y5), x5);
      },
    );

    test(
      'contains() returns true if the option is '
      'containing the given value.',
      () {
        const Option<int> x1 = Value(2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);
        const Option<String> x2 = None();
        expect(x2.contains(2), false);
      },
    );

    test(
      'expect() return value if the option is a Value '
      'else throw Exception',
      () {
        const Option<int> x1 = Value(2);
        expect(x1.expect('Testing expect'), 2);

        const Option<int> x2 = None();
        expect(
          () => x2.expect('Testing expect'),
          throwsA(
            predicate<OptionException>(
              (e) => e.message == ('Testing expect'),
            ),
          ),
        );
      },
    );

    test(
      'map() transforms Value.',
      () {
        const Option<bool> x1 = Value(true);

        expect(x1.get(), true);
        expect(x1.map((value) => false).get(), false);

        const Option<bool> x2 = None();

        expect(x2.map((value) => false).isNull, true);
      },
    );

    test(
      'mapAsync() transforms Value.',
      () async {
        const Option<bool> x1 = Value(true);

        expect(x1.get(), true);
        expect(
          (await x1.mapAsync((value) => Future.value(false))).get(),
          false,
        );

        const Option<bool> x2 = None();

        expect(
          (await x2.mapAsync((value) => Future.value(false))).isNull,
          true,
        );
      },
    );

    test(
      'of(T value) returns Option with Value',
      () {
        final Option<int> x1 = Option.of(2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);
        expect(
          () => Option.of(null),
          throwsA(
            predicate<OptionException>(
              (e) => e.message == ("Value can' be null. Use `ofNullable()`"),
            ),
          ),
        );
      },
    );

    test(
      "ofNullable(T value) returns Option with Value and don't throw on Null",
      () {
        final Option<int> x1 = Option.ofNullable(2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);

        final Option<int> x2 = Option.ofNullable(null);
        expect(x2.isNull, true);
      },
    );

    test(
      'ofLazy() returns Option with Value',
      () {
        final Option<int> x1 = Option.ofLazy(() => 2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);
        expect(
          () => Option.ofLazy(() => null),
          throwsA(
            predicate<OptionException>(
              (e) =>
                  e.message == ("Value can' be null. Use `ofNullableLazy()`"),
            ),
          ),
        );
      },
    );

    test(
      "ofNullableLazy() returns Option with Value and don't throw on Null",
      () {
        final Option<int> x1 = Option.ofNullableLazy(() => 2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);

        final Option<int> x2 = Option.ofNullableLazy(() => null);
        expect(x2.isNull, true);
      },
    );
  });
}
