<?php
/**
 * @author Daniel J. Summers <daniel@bitbadger.solutions>
 * @license MIT
 */

declare(strict_types=1);

namespace Test;

use BitBadger\InspiredByFSharp\Result;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;

/**
 * Unit tests for the Result class
 */
class ResultTest extends TestCase
{
    #[TestDox('GetOK succeeds for OK result')]
    public function testGetOKSucceedsForOKResult(): void
    {
        $result = Result::OK('yay');
        $this->assertEquals('yay', $result->getOK(), 'The OK result should have been returned');
    }

    #[TestDox('GetOK fails for Error result')]
    public function testGetOKFailsForErrorResult(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::Error('whoops')->getOK();
    }

    #[TestDox('GetError succeeds for Error result')]
    public function testGetErrorSucceedsForErrorResult(): void
    {
        $result = Result::Error('boo');
        $this->assertEquals('boo', $result->getError(), 'The Error result should have been returned');
    }

    #[TestDox('GetError fails for OK result')]
    public function testGetErrorFailsForOKResult(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::OK('yeah')->getError();
    }

    #[TestDox('IsOK succeeds for OK result')]
    public function testIsOKSucceedsForOKResult(): void
    {
        $result = Result::OK('ok');
        $this->assertTrue($result->isOK(), 'The check for "OK" should have returned true');
    }

    #[TestDox('IsOK succeeds for Error result')]
    public function testIsOKSucceedsForErrorResult(): void
    {
        $result = Result::Error('error');
        $this->assertFalse($result->isOK(), 'The check for "OK" should have returned false');
    }

    #[TestDox('IsError succeeds for Error result')]
    public function testIsErrorSucceedsForErrorResult(): void
    {
        $result = Result::Error('not ok');
        $this->assertTrue($result->isError(), 'The check for "Error" should have returned true');
    }

    #[TestDox('IsError succeeds for OK result')]
    public function testIsErrorSucceedsForOKResult(): void
    {
        $result = Result::OK('fine');
        $this->assertFalse($result->isError(), 'The check for "Error" should have returned false');
    }

    #[TestDox('Bind succeeds for OK with OK')]
    public function testBindSucceedsForOKWithOK(): void
    {
        $result = Result::OK('one')->bind(fn($it) => Result::OK("$it two"));
        $this->assertTrue($result->isOK(), 'The result should have been OK');
        $this->assertEquals('one two', $result->getOK(), 'The bound function was not called');
    }

    #[TestDox('Bind succeeds for OK with Error')]
    public function testBindSucceedsForOKWithError(): void
    {
        $result = Result::OK('three')->bind(fn($it) => Result::Error('back to two'));
        $this->assertTrue($result->isError(), 'The result should have been Error');
        $this->assertEquals('back to two', $result->getError(), 'The bound function was not called');
    }

    #[TestDox('Bind succeeds for Error')]
    public function testBindSucceedsForError(): void
    {
        $original = Result::Error('oops');
        $result   = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
        $this->assertTrue($result->isError(), 'The result should have been Error');
        $this->assertSame($original, $result, 'The same Error result should have been returned');
    }


    #[TestDox('Contains succeeds for Error result')]
    public function testContainsSucceedsForErrorResult(): void
    {
        $this->assertFalse(Result::Error('ouch')->contains('ouch'), '"Error" should always return false');
    }

    #[TestDox('Contains succeeds for OK result when strictly equal')]
    public function testContainsSucceedsForOKResultWhenStrictlyEqual(): void
    {
        $this->assertTrue(Result::OK(18)->contains(18), '"OK" with strict equality should be true');
    }

    #[TestDox('Contains succeeds for OK result when strictly unequal')]
    public function testContainsSucceedsForOKResultWhenStrictlyUnequal(): void
    {
        $this->assertFalse(Result::OK(18)->contains('18'), '"OK" with strict equality should be false');
    }

    #[TestDox('Contains succeeds for OK result when loosely equal')]
    public function testContainsSucceedsForOKResultWhenLooselyEqual(): void
    {
        $this->assertTrue(Result::OK(18)->contains('18', strict: false), '"OK" with loose equality should be true');
    }

    #[TestDox('Contains succeeds for OK result when loosely unequal')]
    public function testContainsSucceedsForOKResultWhenLooselyUnequal(): void
    {
        $this->assertFalse(Result::OK(18)->contains(17, strict: false), '"OK" with loose equality should be false');
    }

    #[TestDox('Exists succeeds for OK result when matching')]
    public function testExistsSucceedsForOKResultWhenMatching(): void
    {
        $this->assertTrue(Result::OK(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
    }

    #[TestDox('Exists succeeds for OK result when not matching')]
    public function testExistsSucceedsForOKResultWhenNotMatching(): void
    {
        $this->assertFalse(Result::OK(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
    }

    #[TestDox('Exists succeeds for Error result')]
    public function testExistsSucceedsForErrorResult(): void
    {
        $this->assertFalse(Result::Error(true)->exists(fn($it) => true), 'Exists should have returned false');
    }

    #[TestDox('Map succeeds for OK result')]
    public function testMapSucceedsForOKResult(): void
    {
        $ok     = Result::OK('yard');
        $mapped = $ok->map(fn($it) => strrev($it));
        $this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
        $this->assertEquals('dray', $mapped->getOK(), 'The mapping function was not called correctly');
    }

    #[TestDox('Map fails for OK result when mapping is null')]
    public function testMapFailsForOKResultWhenMappingIsNull(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::OK('not null')->map(fn($it) => null);
    }

    #[TestDox('Map succeeds for Error result')]
    public function testMapSucceedsForErrorResult(): void
    {
        $tattle = new class { public bool $called = false; };
        $error  = Result::Error('nope');
        $mapped = $error->map(function () use ($tattle)
        {
            $tattle->called = true;
            return 'hello';
        });
        $this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
        $this->assertFalse($tattle->called, 'The mapping function should not have been called');
        $this->assertSame($error, $mapped, 'The same "Error" instance should have been returned');
    }

    #[TestDox('MapError succeeds for OK result')]
    public function testMapErrorSucceedsForOKResult(): void
    {
        $tattle = new class { public bool $called = false; };
        $ok     = Result::OK('sure');
        $mapped = $ok->mapError(function () use ($tattle)
        {
            $tattle->called = true;
            return 'hello';
        });
        $this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
        $this->assertFalse($tattle->called, 'The mapping function should not have been called');
        $this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned');
    }

    #[TestDox('MapError succeeds for Error result')]
    public function testMapErrorSucceedsForErrorResult(): void
    {
        $error  = Result::Error('taco');
        $mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it)));
        $this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
        $this->assertEquals('****', $mapped->getError(), 'The mapping function was not called correctly');
    }

    #[TestDox('MapError fails for Error result when mapping is null')]
    public function testMapErrorFailsForErrorResultWhenMappingIsNull(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::Error('pizza')->mapError(fn($it) => null);
    }

    #[TestDox('Iter succeeds for OK result')]
    public function testIterSucceedsForOKResult(): void
    {
        $target = new class { public mixed $called = null; };
        Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
        $this->assertEquals(77, $target->called, 'The function should have been called with the "OK" value');
    }

    #[TestDox('Iter succeeds for Error result')]
    public function testIterSucceedsForErrorResult(): void
    {
        $target = new class { public mixed $called = null; };
        Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
        $this->assertNull($target->called, 'The function should not have been called');
    }

    #[TestDox('ToArray succeeds for OK result')]
    public function testToArraySucceedsForOKResult(): void
    {
        $arr = Result::OK('yay')->toArray();
        $this->assertNotNull($arr, 'The array should not have been null');
        $this->assertEquals(['yay'], $arr, 'The array was not created correctly');
    }

    #[TestDox('ToArray succeeds for Error result')]
    public function testToArraySucceedsForErrorResult(): void
    {
        $arr = Result::Error('oh no')->toArray();
        $this->assertNotNull($arr, 'The array should not have been null');
        $this->assertEmpty($arr, 'The array should have been empty');
    }

    #[TestDox('ToOption succeeds for OK result')]
    public function testToOptionSucceedsForOKResult()
    {
        $value = Result::OK(99)->toOption();
        $this->assertTrue($value->isSome(), 'An "OK" result should map to a "Some" option');
        $this->assertEquals(99, $value->get(), 'The value is not correct');
    }

    #[TestDox('ToOption succeeds for Error result')]
    public function testToOptionSucceedsForErrorResult()
    {
        $value = Result::Error('file not found')->toOption();
        $this->assertTrue($value->isNone(), 'An "Error" result should map to a "None" option');
    }

    #[TestDox('Tap succeeds for OK result')]
    public function testTapSucceedsForOKResult(): void
    {
        $value    = '';
        $original = Result::OK('working');
        $tapped   = $original->tap(function (Result $it) use (&$value) {
            $value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
        });
        $this->assertEquals('OK: working', $value, 'The tapped function was not called');
        $this->assertSame($original, $tapped, 'The same result should have been returned');
    }

    #[TestDox('Tap succeeds for Error result')]
    public function testTapSucceedsForErrorResult(): void
    {
        $value    = '';
        $original = Result::Error('failed');
        $tapped   = $original->tap(function (Result $it) use (&$value) {
            $value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
        });
        $this->assertEquals('Error: failed', $value, 'The tapped function was not called');
        $this->assertSame($original, $tapped, 'The same result should have been returned');
    }

    #[TestDox('OK succeeds for non null result')]
    public function testOKSucceedsForNonNullResult(): void
    {
        $result = Result::OK('something');
        $this->assertTrue($result->isOK(), 'The result should have been "OK"');
        $this->assertEquals('something', $result->getOK(), 'The "OK" value was incorrect');
    }

    #[TestDox('OK fails for null result')]
    public function testOKFailsForNullResult(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::OK(null);
    }

    #[TestDox('Error succeeds for non null result')]
    public function testErrorSucceedsForNonNullResult(): void
    {
        $result = Result::Error('sad trombone');
        $this->assertTrue($result->isError(), 'The result should have been "Error"');
        $this->assertEquals('sad trombone', $result->getError(), 'The "Error" value was incorrect');
    }

    #[TestDox('Error fails for null result')]
    public function testErrorFailsForNullResult(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Result::Error(null);
    }
}