// 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('Result<T,E>', () {
    test('`isOk` returns true on Ok value', () {
      expect(
        const Ok<void, void>(null).isOk,
        true,
      );
      expect(
        const Err<void, void>(null).isOk,
        false,
      );
    });

    test('`isErr` returns true on Err value', () {
      expect(
        const Ok<void, void>(null).isErr,
        false,
      );
      expect(
        const Err<void, void>(null).isErr,
        true,
      );
    });

    test('`ok` returns value on Ok value', () {
      const Result<int, String> x = Ok(2);
      expect(x.ok, 2);
      expect(x.err, null);
    });

    test('`err` returns error on Err value', () {
      const Result<int, String> x = Err('error');
      expect(x.ok, null);
      expect(x.err, 'error');
    });

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

    test('unwrap() returns error on Err value', () {
      const Result<int, String> x = Err('error');
      expect(x.unwrap<String>(), 'error');
      expect(() => x.unwrap<int>(), throwsA(isException));
    });

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

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

    test('fold() returns right value', () {
      expect(
        const Ok<String, String>('')
            .fold<bool>((left) => true, (right) => false),
        true,
      );
      expect(
        const Err<String, String>('')
            .fold<bool>((left) => true, (right) => false),
        false,
      );
    });

    test('foldAsync() returns right value', () async {
      expect(
        await const Ok<String, String>('').foldAsync<bool>(
          (left) => Future.value(true),
          (right) => Future.value(false),
        ),
        true,
      );
      expect(
        await const Err<String, String>('').foldAsync<bool>(
          (left) => Future.value(true),
          (right) => Future.value(false),
        ),
        false,
      );
    });

    test('swap() swaps values', () {
      const Result<int, int> x = Ok(10);
      expect(x.isOk, true);
      expect(x.isErr, false);
      expect(x.ok, 10);
      final Result<int, int> y = x.swap();
      expect(y.isOk, false);
      expect(y.isErr, true);
      expect(y.err, 10);
    });

    test(
      'and() returns res if the result is `Ok`, otherwise '
      'returns the `Err` value of this.',
      () {
        const Result<int, String> x1 = Ok(2);
        const Result<int, String> y1 = Err('late error');
        expect(x1.and(y1), y1);

        const Result<String, String> x2 = Err('early error');
        const Result<String, String> y2 = Ok('foo');
        expect(x2.and(y2), x2);

        const Result<void, String> x3 = Err('not a 2');
        const Result<void, String> y3 = Err('late error');
        expect(x3.and(y3), x3);

        const Result<dynamic, void> x4 = Ok(2);
        const Result<dynamic, void> y4 = Ok('different result type');
        expect(x4.and(y4), y4);

        const Result<int, void> x5 = Ok(2);
        const Result<int, void> y5 = Ok(5);
        expect(x5.and(y5), y5);
      },
    );

    test(
      'or() returns res if the result is `Err`, otherwise '
      'returns the `Ok` value of this.',
      () {
        const Result<int, String> x1 = Ok(2);
        const Result<int, String> y1 = Err('late error');
        expect(x1.or(y1), x1);

        const Result<String, String> x2 = Err('early error');
        const Result<String, String> y2 = Ok('foo');
        expect(x2.or(y2), y2);

        const Result<void, String> x3 = Err('not a 2');
        const Result<void, String> y3 = Err('late error');
        expect(x3.or(y3), y3);

        const Result<dynamic, void> x4 = Ok(2);
        const Result<dynamic, void> y4 = Ok('different result type');
        expect(x4.or(y4), x4);

        const Result<int, void> x5 = Ok(2);
        const Result<int, void> y5 = Ok(5);
        expect(x5.or(y5), x5);
      },
    );

    test(
      'contains() returns true if the result is an Ok value '
      'containing the given value.',
      () {
        const Result<int, void> x1 = Ok(2);
        expect(x1.contains(2), true);
        expect(x1.contains(3), false);
        const Result<void, String> x2 = Err('Some error message');
        expect(x2.contains(2), false);
      },
    );

    test(
      'expect() return value if the result is an Ok value '
      'else throw Exception',
      () {
        const Result<int, String> x1 = Ok(2);
        expect(x1.expect('Testing expect'), 2);

        const Result<int, String> x2 = Err('emergency failure');
        expect(
          () => x2.expect('Testing expect'),
          throwsA(
            predicate<ResultException>(
              (e) => e.message == ('Testing expect: emergency failure'),
            ),
          ),
        );
      },
    );

    test(
      'expectErr() return value if the result is an Err value '
      'else throw Exception',
      () {
        const Result<String, int> x1 = Err(2);
        expect(x1.expectErr('Testing expect err'), 2);

        const Result<String, int> x2 = Ok('success value');
        expect(
          () => x2.expectErr('Testing expect err'),
          throwsA(
            predicate<ResultException>(
              (e) => e.message == ('Testing expect err: success value'),
            ),
          ),
        );
      },
    );

    test(
      'map() transforms Ok value.',
      () {
        const Result<bool, void> x1 = Ok(true);

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

        const Result<bool, String> x2 = Err('oops');

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

    test(
      'mapErr() transforms Err value.',
      () {
        const Result<bool, String> x1 = Ok(true);

        expect(x1.mapErr((value) => false).isOk, true);

        const Result<bool, String> x2 = Err('oops');

        expect(x2.err, 'oops');
        expect(x2.mapErr((error) => 'failure').err, 'failure');
      },
    );

    test(
      'mapAsync() transforms Ok value.',
      () async {
        const Result<bool, void> x1 = Ok(true);

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

        const Result<bool, String> x2 = Err('oops');

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

    test(
      'mapErrAsync() transforms Err value.',
      () async {
        const Result<bool, String> x1 = Ok(true);

        expect(
          (await x1.mapErrAsync((value) => Future.value(false))).isOk,
          true,
        );

        const Result<bool, String> x2 = Err('oops');

        expect(x2.err, 'oops');
        expect(
          (await x2.mapErrAsync((error) => Future.value('failure'))).err,
          'failure',
        );
      },
    );

    test(
      'either() transforms values.',
      () {
        const Result<bool, bool> x1 = Ok(true);
        final Result<int, String> y1 =
            x1.either<int, String>((value) => 1, (error) => 'error');

        expect(y1.isOk, true);
        expect(y1.ok, 1);

        const Result<bool, bool> x2 = Err(true);
        final Result<int, String> y2 =
            x2.either<int, String>((value) => 1, (error) => 'error');

        expect(y2.isErr, true);
        expect(y2.err, 'error');
      },
    );

    test(
      'eitherAsync() transforms values.',
      () async {
        const Result<bool, bool> x1 = Ok(true);
        final Result<int, String> y1 = await x1.eitherAsync<int, String>(
          (value) => Future.value(1),
          (error) => Future.value('error'),
        );

        expect(y1.isOk, true);
        expect(y1.ok, 1);

        const Result<bool, bool> x2 = Err(true);
        final Result<int, String> y2 = await x2.eitherAsync<int, String>(
          (value) => Future.value(1),
          (error) => Future.value('error'),
        );

        expect(y2.isErr, true);
        expect(y2.err, 'error');
      },
    );

    test(
      'tryCatch() try and catch error as Err.',
      () {
        expect(
          Result.tryCatch<int, String, Exception>(
            () => throw Exception('not success'),
            (err) => 'error',
          ).isErr,
          true,
        );
        expect(
          Result.tryCatch<int, String, Exception>(
            () => 2,
            (err) => 'error',
          ).isOk,
          true,
        );
      },
    );

    test(
      'tryCatchAsync() try and catch error as Err.',
      () async {
        expect(
          (await Result.tryCatchAsync<int, String, Exception>(
            () => throw Exception('not success'),
            (err) => 'error',
          )).isErr,
          true,
        );
        expect(
          (await Result.tryCatchAsync<int, String, Exception>(
            () => Future.value(2),
            (err) => 'error',
          )).isOk,
          true,
        );
      },
    );

    test(
      'conditional() returns Result on true test',
      () {
        final Result<int, String> x1 = Result.conditional<int, String>(
          false,
          2,
          'error',
        );
        expect(
          x1.isErr,
          true,
        );

        expect(x1.err, 'error');

        final Result<int, String> x2 = Result.conditional<int, String>(
          true,
          2,
          'error',
        );
        expect(
          x2.isOk,
          true,
        );

        expect(x2.ok, 2);
      },
    );

    test(
      'conditionalLazy() returns Result on true test',
      () {
        final Result<int, String> x1 = Result.conditionalLazy<int, String>(
          false,
          () => 2,
          () => 'error',
        );
        expect(
          x1.isErr,
          true,
        );

        expect(x1.err, 'error');

        final Result<int, String> x2 = Result.conditionalLazy<int, String>(
          true,
          () => 2,
          () => 'error',
        );
        expect(
          x2.isOk,
          true,
        );

        expect(x2.ok, 2);
      },
    );
  });
}
