<?php declare(strict_types=1);
/*
 * This file is part of PHPUnit.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace PHPUnit\Framework\MockObject;

use function class_exists;
use function md5;
use function method_exists;
use function microtime;
use Countable;
use Exception;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
use PHPUnit\Framework\MockObject\Rule\MethodName;
use PHPUnit\Framework\MockObject\Stub\ReturnStub;
use PHPUnit\Framework\TestCase;
use PHPUnit\TestFixture\AbstractTrait;
use PHPUnit\TestFixture\ExceptionWithThrowable;
use PHPUnit\TestFixture\MockObject\AbstractMockTestClass;
use PHPUnit\TestFixture\MockObject\AnInterface;
use PHPUnit\TestFixture\MockObject\AnInterfaceWithReturnType;
use PHPUnit\TestFixture\MockObject\AnotherInterface;
use PHPUnit\TestFixture\MockObject\ClassWithVariadicArgumentMethod;
use PHPUnit\TestFixture\MockObject\FinalClass;
use PHPUnit\TestFixture\MockObject\InterfaceWithSemiReservedMethodName;
use PHPUnit\TestFixture\MockObject\ReadonlyClass;
use PHPUnit\TestFixture\SingletonClass;
use RuntimeException;
use stdClass;
use Throwable;

#[CoversClass(Generator::class)]
#[UsesClass(InvocationMocker::class)]
#[UsesClass(Invocation::class)]
#[UsesClass(InvocationHandler::class)]
#[UsesClass(Matcher::class)]
#[UsesClass(InvocationOrder::class)]
#[UsesClass(InvokedCount::class)]
#[UsesClass(MethodName::class)]
#[UsesClass(ReturnStub::class)]
#[Small]
final class GeneratorTest extends TestCase
{
    private ?Generator $generator;

    protected function setUp(): void
    {
        $this->generator = new Generator;
    }

    protected function tearDown(): void
    {
        $this->generator = null;
    }

    public function testGetMockThrowsExceptionWhenInvalidFunctionNameIsPassedInAsAFunctionToMock(): void
    {
        $this->expectException(InvalidMethodNameException::class);

        $this->generator->getMock(stdClass::class, [0]);
    }

    public function testGetMockThrowsExceptionWithNonExistingClass(): void
    {
        $this->expectException(UnknownTypeException::class);

        $this->assertFalse(class_exists('Tux'));

        $this->generator->getMock('Tux', [], [], '', true, true, false, true, false, null, false);
    }

    public function testGetMockThrowsExceptionWithNonExistingClasses(): void
    {
        $this->expectException(UnknownTypeException::class);

        $this->assertFalse(class_exists('Tux'));

        $this->generator->getMock('Tux', [], [], '', true, true, false, true, false, null, false);
    }

    public function testGetMockThrowsExceptionWithExistingClassAsMockName(): void
    {
        $this->expectException(ClassAlreadyExistsException::class);

        $this->generator->getMock(stdClass::class, [], [], RuntimeException::class);
    }

    public function testGetMockCanCreateNonExistingFunctions(): void
    {
        $mock = $this->generator->getMock(stdClass::class, ['testFunction']);

        $this->assertTrue(method_exists($mock, 'testFunction'));
    }

    public function testGetMockGeneratorThrowsException(): void
    {
        $this->expectException(DuplicateMethodException::class);
        $this->expectExceptionMessage('duplicates: "foo, bar, foo" (duplicate: "foo")');

        $this->generator->getMock(stdClass::class, ['foo', 'bar', 'foo']);
    }

    public function testGetMockBlacklistedMethodNamesPhp7(): void
    {
        $mock = $this->generator->getMock(InterfaceWithSemiReservedMethodName::class);

        $this->assertTrue(method_exists($mock, 'unset'));
        $this->assertInstanceOf(InterfaceWithSemiReservedMethodName::class, $mock);
    }

    public function testGetMockForAbstractClassDoesNotFailWhenFakingInterfaces(): void
    {
        $mock = $this->generator->getMockForAbstractClass(Countable::class);

        $this->assertTrue(method_exists($mock, 'count'));
    }

    public function testGetMockForAbstractClassStubbingAbstractClass(): void
    {
        $mock = $this->generator->getMockForAbstractClass(AbstractMockTestClass::class);

        $this->assertTrue(method_exists($mock, 'doSomething'));
    }

    public function testGetMockForAbstractClassWithNonExistentMethods(): void
    {
        $mock = $this->generator->getMockForAbstractClass(
            AbstractMockTestClass::class,
            [],
            '',
            true,
            true,
            true,
            ['nonexistentMethod']
        );

        $this->assertTrue(method_exists($mock, 'nonexistentMethod'));
        $this->assertTrue(method_exists($mock, 'doSomething'));
    }

    public function testGetMockForAbstractClassShouldCreateStubsOnlyForAbstractMethodWhenNoMethodsWereInformed(): void
    {
        $mock = $this->generator->getMockForAbstractClass(AbstractMockTestClass::class);

        $mock->method('doSomething')
             ->willReturn('testing');

        $this->assertEquals('testing', $mock->doSomething());
        $this->assertEquals(1, $mock->returnAnything());
    }

    public function testGetMockForAbstractClassAbstractClassDoesNotExist(): void
    {
        $this->expectException(UnknownClassException::class);

        $this->generator->getMockForAbstractClass('Tux');
    }

    public function testGetMockForTraitWithNonExistentMethodsAndNonAbstractMethods(): void
    {
        $mock = $this->generator->getMockForTrait(
            AbstractTrait::class,
            [],
            '',
            true,
            true,
            true,
            ['nonexistentMethod']
        );

        $this->assertTrue(method_exists($mock, 'nonexistentMethod'));
        $this->assertTrue(method_exists($mock, 'doSomething'));
        $this->assertTrue($mock->mockableMethod());
        $this->assertTrue($mock->anotherMockableMethod());
    }

    public function testGetMockForTraitStubbingAbstractMethod(): void
    {
        $mock = $this->generator->getMockForTrait(AbstractTrait::class);

        $this->assertTrue(method_exists($mock, 'doSomething'));
    }

    public function testGetMockForTraitWithNonExistantTrait(): void
    {
        $this->expectException(UnknownTraitException::class);

        $mock = $this->generator->getMockForTrait('Tux');
    }

    public function testGetObjectForTraitWithNonExistantTrait(): void
    {
        $this->expectException(UnknownTraitException::class);

        $mock = $this->generator->getObjectForTrait('Tux');
    }

    public function testGetMockClassMethodsForNonExistantClass(): void
    {
        $this->expectException(ReflectionException::class);

        $mock = $this->generator->mockClassMethods('Tux', true, true);
    }

    public function testGetMockForSingletonWithReflectionSuccess(): void
    {
        $mock = $this->generator->getMock(SingletonClass::class, ['doSomething'], [], '', false);

        $this->assertInstanceOf(SingletonClass::class, $mock);
    }

    public function testExceptionIsRaisedForMutuallyExclusiveOptions(): void
    {
        $this->expectException(OriginalConstructorInvocationRequiredException::class);

        $this->generator->getMock(stdClass::class, [], [], '', false, true, true, true, true);
    }

    public function testCanImplementInterfacesThatHaveMethodsWithReturnTypes(): void
    {
        $stub = $this->generator->getMock(AnInterfaceWithReturnType::class);

        $this->assertInstanceOf(AnInterfaceWithReturnType::class, $stub);
        $this->assertInstanceOf(MockObject::class, $stub);
    }

    public function testCanConfigureMethodsForDoubleOfNonExistentClass(): void
    {
        $className = 'X' . md5(microtime());

        $mock = $this->generator->getMock($className, ['someMethod']);

        $this->assertInstanceOf($className, $mock);
    }

    public function testCanInvokeMethodsOfNonExistentClass(): void
    {
        $className = 'X' . md5(microtime());

        $mock = $this->generator->getMock($className, ['someMethod']);

        $mock->expects($this->once())->method('someMethod');

        $this->assertNull($mock->someMethod());
    }

    public function testMockingOfExceptionWithThrowable(): void
    {
        $stub = $this->generator->getMock(ExceptionWithThrowable::class);

        $this->assertInstanceOf(ExceptionWithThrowable::class, $stub);
        $this->assertInstanceOf(Exception::class, $stub);
        $this->assertInstanceOf(MockObject::class, $stub);
    }

    public function testMockingOfThrowable(): void
    {
        $stub = $this->generator->getMock(Throwable::class);

        $this->assertInstanceOf(Throwable::class, $stub);
        $this->assertInstanceOf(Exception::class, $stub);
        $this->assertInstanceOf(MockObject::class, $stub);
    }

    public function testMockingOfThrowableConstructorArguments(): void
    {
        $mock = $this->generator->getMock(Throwable::class, null, ['It works']);
        $this->assertSame('It works', $mock->getMessage());
    }

    public function testVariadicArgumentsArePassedToOriginalMethod(): void
    {
        /** @var ClassWithVariadicArgumentMethod|MockObject $mock */
        $mock = $this->generator->getMock(
            ClassWithVariadicArgumentMethod::class,
            [],
            [],
            '',
            true,
            false,
            true,
            false,
            true
        );

        $arguments = [1, 'foo', false];
        $this->assertSame($arguments, $mock->foo(...$arguments));
    }

    public function testVariadicArgumentsArePassedToMockedMethod(): void
    {
        /** @var ClassWithVariadicArgumentMethod|MockObject $mock */
        $mock = $this->createMock(ClassWithVariadicArgumentMethod::class);

        $arguments = [1, 'foo', false];
        $mock->expects($this->once())
            ->method('foo')
            ->with(...$arguments);

        $mock->foo(...$arguments);
    }

    public function testGetClassMethodsWithNonExistingClass(): void
    {
        $this->expectException(ReflectionException::class);

        $this->generator->getClassMethods('Tux');
    }

    public function testCannotMockFinalClass(): void
    {
        $this->expectException(ClassIsFinalException::class);

        /* @noinspection ClassMockingCorrectnessInspection */
        $this->createMock(FinalClass::class);
    }

    #[RequiresPhp('8.2')]
    public function testCannotMockReadonlyClass(): void
    {
        $this->expectException(ClassIsReadonlyException::class);

        /* @noinspection ClassMockingCorrectnessInspection */
        $this->createMock(ReadonlyClass::class);
    }

    public function testCanDoubleIntersectionOfMultipleInterfaces(): void
    {
        $stub = $this->generator->getMockForInterfaces(
            [
                AnInterface::class,
                AnotherInterface::class,
            ]
        );

        $this->assertInstanceOf(AnInterface::class, $stub);
        $this->assertInstanceOf(AnotherInterface::class, $stub);

        $stub->method('doSomething')->willReturn(true);
        $stub->method('doSomethingElse')->willReturn(false);

        $this->assertTrue($stub->doSomething());
        $this->assertFalse($stub->doSomethingElse());
    }
}
