<?php
declare(strict_types=1);

namespace Apexl\Vault\Tests;

use Apexl\Vault\Interfaces\Driver;
use Apexl\Vault\Interfaces\Operators;
use Apexl\Vault\Interfaces\QueryBuilderInterface;
use Apexl\Vault\Interfaces\ResultInterface;
use Apexl\Vault\Operators\Drivers\MongoDb;
use Apexl\Vault\Operators\Drivers\MySql;
use Apexl\Vault\Operators\Drivers\SqLite;
use Apexl\Vault\Results\ArrayResult;
use Apexl\Vault\Results\PdoResult;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use stdClass;
use TypeError;

/**
 * The 4.x names were first kept with class_alias(), which does not work: PHP resolves a
 * declared type by comparing names against the interfaces the object's class already
 * lists, and never autoloads the declared type to do it. An aliased name was therefore
 * invisible to type declarations — a `driver` parameter fataled and `instanceof driver`
 * returned false — while interface_exists() and is_a() happily reported it fine.
 *
 * So these assert against a real type declaration. Asserting is_a() alone is what let
 * the broken version pass.
 */
final class InterfaceNamingTest extends TestCase
{
    public static function drivers(): array
    {
        return [
            'mysql' => [MySql::class],
            'sqlite' => [SqLite::class],
            'mongodb' => [MongoDb::class],
        ];
    }

    private function sqliteDriver(): SqLite
    {
        if (!extension_loaded('pdo_sqlite')) {
            $this->markTestSkipped('ext-pdo_sqlite not available');
        }

        $cfg = new stdClass();
        $cfg->driver = 'sqlite';

        return new SqLite($cfg);
    }

    /** The case class_alias() could not serve. */
    #[Test]
    public function aFourPointXTypeDeclarationStillAcceptsADriver(): void
    {
        $takesOldName = static fn(Driver $query): string => $query::class;

        $this->assertSame(SqLite::class, $takesOldName($this->sqliteDriver()));
    }

    #[Test]
    public function instanceofAgainstTheOldNameIsTrue(): void
    {
        $this->assertInstanceOf(Driver::class, $this->sqliteDriver());
        $this->assertInstanceOf(Operators::class, $this->sqliteDriver());
    }

    #[Test]
    public function theNewNameIsSatisfiedToo(): void
    {
        $takesNewName = static fn(QueryBuilderInterface $query): string => $query::class;

        $this->assertSame(SqLite::class, $takesNewName($this->sqliteDriver()));
    }

    #[Test]
    #[DataProvider('drivers')]
    public function everyDriverDeclaresBothNamesItself(string $class): void
    {
        // Declared on the class, not inherited via an alias — this is the only thing
        // PHP consults when checking a type.
        $interfaces = (new ReflectionClass($class))->getInterfaceNames();

        $this->assertContains(QueryBuilderInterface::class, $interfaces);
        $this->assertContains(Driver::class, $interfaces);
        $this->assertContains(Operators::class, $interfaces);
    }

    #[Test]
    #[DataProvider('drivers')]
    public function theOldNamesAreMarkedDeprecated(string $class): void
    {
        foreach ([Driver::class, Operators::class] as $old) {
            $this->assertStringContainsString(
                '@deprecated',
                (string) (new ReflectionClass($old))->getDocComment(),
                sprintf('%s carries no deprecation for consumers of %s', $old, $class),
            );
        }
    }

    #[Test]
    public function resultsAreNotBuilders(): void
    {
        foreach ([PdoResult::class, ArrayResult::class] as $result) {
            $this->assertTrue(is_a($result, ResultInterface::class, true));
            $this->assertFalse(
                is_a($result, QueryBuilderInterface::class, true),
                'a result must not also be a builder — that is the aliasing this split removed',
            );
        }
    }

    /** A result is not silently acceptable where a builder is asked for, or vice versa. */
    #[Test]
    public function theTwoContractsDoNotSubstituteForEachOther(): void
    {
        $takesResult = static fn(ResultInterface $rows): string => $rows::class;

        $this->expectException(TypeError::class);
        $takesResult($this->sqliteDriver());
    }

    /** The merge: everything that survived from `operators` is on the builder. */
    #[Test]
    public function theBuilderCarriesTheExecutionContract(): void
    {
        foreach (['execute', 'executeRaw', 'isCount', 'getQueryFields'] as $method) {
            $this->assertTrue(
                method_exists(QueryBuilderInterface::class, $method),
                sprintf('%s() went missing in the merge', $method),
            );
        }
    }
}
