<?php

declare(strict_types=1);

namespace Tests\StaticPHP\Artifact;

use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;

/**
 * @internal
 */
class ArtifactTest extends TestCase
{
    private string $tempDir;

    protected function setUp(): void
    {
        parent::setUp();
        $this->tempDir = sys_get_temp_dir() . '/artifact_test_' . uniqid();
        mkdir($this->tempDir, 0755, true);

        // Reset ArtifactConfig static state
        $reflection = new \ReflectionClass(ArtifactConfig::class);
        $property = $reflection->getProperty('artifact_configs');
        $property->setAccessible(true);
        $property->setValue(null, []);

        // Reset DI container
        ApplicationContext::reset();
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        if (is_dir($this->tempDir)) {
            $this->removeDirectory($this->tempDir);
        }

        $reflection = new \ReflectionClass(ArtifactConfig::class);
        $property = $reflection->getProperty('artifact_configs');
        $property->setAccessible(true);
        $property->setValue(null, []);

        ApplicationContext::reset();
    }

    // ==================== Constants ====================

    public function testConstantValues(): void
    {
        $this->assertSame(0, Artifact::FETCH_PREFER_SOURCE);
        $this->assertSame(1, Artifact::FETCH_PREFER_BINARY);
        $this->assertSame(2, Artifact::FETCH_ONLY_SOURCE);
        $this->assertSame(3, Artifact::FETCH_ONLY_BINARY);
    }

    // ==================== Constructor ====================

    public function testConstructWithInlineConfig(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertSame('my-pkg', $artifact->getName());
    }

    public function testConstructFallsBackToArtifactConfig(): void
    {
        $this->injectArtifactConfig('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $artifact = new Artifact('my-pkg');
        $this->assertSame('my-pkg', $artifact->getName());
    }

    public function testConstructThrowsForNonExistentArtifact(): void
    {
        $this->expectException(WrongUsageException::class);
        $this->expectExceptionMessage("Artifact 'non-existent' not found.");

        new Artifact('non-existent');
    }

    // ==================== getName ====================

    public function testGetName(): void
    {
        $artifact = new Artifact('openssl', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $this->assertSame('openssl', $artifact->getName());
    }

    // ==================== getDownloadConfig ====================

    public function testGetDownloadConfigReturnsSection(): void
    {
        $config = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], 'binary' => []];
        $artifact = new Artifact('my-pkg', $config);

        $this->assertSame(['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], $artifact->getDownloadConfig('source'));
    }

    public function testGetDownloadConfigReturnsNullForMissingSection(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertNull($artifact->getDownloadConfig('non-existent'));
    }

    // ==================== hasSource ====================

    public function testHasSourceReturnsTrueWhenConfigHasSource(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertTrue($artifact->hasSource());
    }

    public function testHasSourceReturnsFalseWhenNoSource(): void
    {
        $artifact = new Artifact('my-pkg', ['binary' => []]);

        $this->assertFalse($artifact->hasSource());
    }

    public function testHasSourceReturnsTrueWithCustomCallback(): void
    {
        $artifact = new Artifact('my-pkg', ['binary' => []]);
        $artifact->setCustomSourceCallback(function () {});

        $this->assertTrue($artifact->hasSource());
    }

    // ==================== hasPlatformBinary ====================

    public function testHasPlatformBinaryReturnsTrueWhenConfigHasBinaryForCurrentPlatform(): void
    {
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
        ]);

        $this->assertTrue($artifact->hasPlatformBinary());
    }

    public function testHasPlatformBinaryReturnsFalseWhenNoBinaryConfig(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertFalse($artifact->hasPlatformBinary());
    }

    public function testHasPlatformBinaryReturnsTrueWithCustomCallback(): void
    {
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $artifact->setCustomBinaryCallback($platform, function () {});

        $this->assertTrue($artifact->hasPlatformBinary());
    }

    // ==================== getBinaryPlatforms ====================

    public function testGetBinaryPlatformsReturnsConfiguredPlatforms(): void
    {
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [
                'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/linux.tar.gz'],
                'macos-aarch64' => ['type' => 'url', 'url' => 'https://example.com/mac.tar.gz'],
            ],
        ]);

        $platforms = $artifact->getBinaryPlatforms();
        $this->assertContains('linux-x86_64', $platforms);
        $this->assertContains('macos-aarch64', $platforms);
    }

    public function testGetBinaryPlatformsExcludesCustomTypeWithoutCallback(): void
    {
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [
                'linux-x86_64' => ['type' => 'custom'],
            ],
        ]);

        // No custom callback registered, so custom-type platform should NOT be included
        $platforms = $artifact->getBinaryPlatforms();
        $this->assertNotContains('linux-x86_64', $platforms);
    }

    public function testGetBinaryPlatformsIncludesCustomTypeWhenCallbackRegistered(): void
    {
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [
                'linux-x86_64' => ['type' => 'custom'],
            ],
        ]);
        $artifact->setCustomBinaryCallback('linux-x86_64', function () {});

        $platforms = $artifact->getBinaryPlatforms();
        $this->assertContains('linux-x86_64', $platforms);
    }

    public function testGetBinaryPlatformsIncludesCustomCallbackPlatforms(): void
    {
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
        ]);
        $artifact->setCustomBinaryCallback('linux-x86_64', function () {});

        $platforms = $artifact->getBinaryPlatforms();
        $this->assertContains('linux-x86_64', $platforms);
    }

    // ==================== getSourceDir ====================

    public function testGetSourceDirDefaultsToSourcePathWithName(): void
    {
        $cache = $this->makeStubbedArtifactCache([]);
        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg');
        $this->assertSame($expected, $artifact->getSourceDir());
    }

    public function testGetSourceDirWithRelativeExtractInConfig(): void
    {
        $cache = $this->makeStubbedArtifactCache([]);
        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => 'my-pkg-1.0'],
        ]);

        $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg-1.0');
        $this->assertSame($expected, $artifact->getSourceDir());
    }

    public function testGetSourceDirWithAbsoluteExtractInConfig(): void
    {
        $cache = $this->makeStubbedArtifactCache([]);
        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        // Use a platform-appropriate absolute path: a Unix-style /tmp/... isn't
        // absolute on Windows (no drive letter), so the test would otherwise
        // fall through into the SOURCE_PATH-prefixed branch on Windows.
        $extract = DIRECTORY_SEPARATOR === '\\' ? 'C:\tmp\my-pkg-extract' : '/tmp/my-pkg-extract';

        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => $extract],
        ]);

        $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $extract);
        $this->assertSame($expected, $artifact->getSourceDir());
    }

    // ==================== getSourceRoot ====================

    public function testGetSourceRootDefaultsToSourceDir(): void
    {
        $cache = $this->makeStubbedArtifactCache([]);
        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertSame($artifact->getSourceDir(), $artifact->getSourceRoot());
    }

    public function testGetSourceRootUsesMetadataSourceRoot(): void
    {
        $cache = $this->makeStubbedArtifactCache([]);
        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'metadata' => ['source-root' => 'src'],
        ]);

        $expected = $artifact->getSourceDir() . DIRECTORY_SEPARATOR . 'src';
        $this->assertSame($expected, $artifact->getSourceRoot());
    }

    // ==================== getBinaryExtractConfig ====================

    public function testGetBinaryExtractConfigDefaultsToStandard(): void
    {
        putenv('EMULATE_PLATFORM=linux-x86_64');
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            // no binary for linux-x86_64
        ]);

        $config = $artifact->getBinaryExtractConfig();
        $this->assertSame('standard', $config['mode']);
        $this->assertSame(PKG_ROOT_PATH, $config['path']);
        putenv('EMULATE_PLATFORM');
    }

    public function testGetBinaryExtractConfigWithHostedExtractReturnsBuildRootPath(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $config = $artifact->getBinaryExtractConfig(['extract' => 'hosted']);
        $this->assertSame('standard', $config['mode']);
        $this->assertSame(BUILD_ROOT_PATH, $config['path']);
    }

    public function testGetBinaryExtractConfigWithRelativeExtractInCacheInfo(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $config = $artifact->getBinaryExtractConfig(['extract' => 'subdir']);
        $this->assertSame('standard', $config['mode']);
        $this->assertStringContainsString('subdir', $config['path']);
    }

    public function testGetBinaryExtractConfigWithAbsoluteExtractInCacheInfo(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $config = $artifact->getBinaryExtractConfig(['extract' => '/tmp/absolute-path']);
        $this->assertSame('standard', $config['mode']);
        $this->assertStringContainsString('absolute-path', $config['path']);
    }

    public function testGetBinaryExtractConfigWithArrayReturnsSelective(): void
    {
        putenv('EMULATE_PLATFORM=linux-x86_64');
        $fileMap = ['lib/libfoo.a' => '/usr/local/lib/libfoo.a'];
        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [
                'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz', 'extract' => $fileMap],
            ],
        ]);

        $config = $artifact->getBinaryExtractConfig();
        $this->assertSame('selective', $config['mode']);
        $this->assertNull($config['path']);
        $this->assertSame($fileMap, $config['files']);
        putenv('EMULATE_PLATFORM');
    }

    // ==================== getBinaryDir ====================

    public function testGetBinaryDirDelegatesToGetBinaryExtractConfig(): void
    {
        putenv('EMULATE_PLATFORM=linux-x86_64');
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertSame(PKG_ROOT_PATH, $artifact->getBinaryDir());
        putenv('EMULATE_PLATFORM');
    }

    // ==================== Custom source callbacks ====================

    public function testSetAndGetCustomSourceCallback(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setCustomSourceCallback($cb);

        $this->assertSame($cb, $artifact->getCustomSourceCallback());
    }

    public function testGetCustomSourceCallbackReturnsNullWhenNotSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertNull($artifact->getCustomSourceCallback());
    }

    public function testSetAndGetCustomSourceCheckUpdateCallback(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setCustomSourceCheckUpdateCallback($cb);

        $this->assertSame($cb, $artifact->getCustomSourceCheckUpdateCallback());
    }

    public function testGetCustomSourceCheckUpdateCallbackReturnsNullWhenNotSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertNull($artifact->getCustomSourceCheckUpdateCallback());
    }

    // ==================== Custom binary callbacks ====================

    public function testSetAndGetCustomBinaryCallback(): void
    {
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setCustomBinaryCallback($platform, $cb);

        $this->assertSame($cb, $artifact->getCustomBinaryCallback());
    }

    public function testGetCustomBinaryCallbackReturnsNullWhenNotSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertNull($artifact->getCustomBinaryCallback());
    }

    public function testSetCustomBinaryCallbackThrowsForInvalidPlatform(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->expectException(ValidationException::class);
        $artifact->setCustomBinaryCallback('invalid-platform-string', function () {});
    }

    public function testSetAndGetCustomBinaryCheckUpdateCallback(): void
    {
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setCustomBinaryCheckUpdateCallback($platform, $cb);

        $this->assertSame($cb, $artifact->getCustomBinaryCheckUpdateCallback());
    }

    public function testSetCustomBinaryCheckUpdateCallbackThrowsForInvalidPlatform(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->expectException(ValidationException::class);
        $artifact->setCustomBinaryCheckUpdateCallback('bad-platform', function () {});
    }

    // ==================== Source extract callbacks ====================

    public function testSetAndGetSourceExtractCallback(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setSourceExtractCallback($cb);

        $this->assertSame($cb, $artifact->getSourceExtractCallback());
    }

    public function testHasSourceExtractCallbackReturnsFalseWhenNotSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertFalse($artifact->hasSourceExtractCallback());
    }

    public function testHasSourceExtractCallbackReturnsTrueWhenSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $artifact->setSourceExtractCallback(function () {});

        $this->assertTrue($artifact->hasSourceExtractCallback());
    }

    // ==================== Binary extract callbacks ====================

    public function testSetAndGetBinaryExtractCallbackForCurrentPlatform(): void
    {
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setBinaryExtractCallback($cb, [$platform]);

        $this->assertSame($cb, $artifact->getBinaryExtractCallback());
    }

    public function testGetBinaryExtractCallbackReturnsNullForNonMatchingPlatform(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        // Register callback for a platform that is definitely NOT the current one
        $otherPlatforms = array_diff(['linux-x86_64', 'linux-aarch64', 'macos-x86_64', 'macos-aarch64', 'windows-x86_64'], [$this->getCurrentPlatform()]);
        $artifact->setBinaryExtractCallback(function () {}, array_values($otherPlatforms));

        // Only returns null if none of the listed platforms match current
        // Since current platform is excluded, all remaining are "other"
        $this->assertNull($artifact->getBinaryExtractCallback());
    }

    public function testSetBinaryExtractCallbackWithEmptyPlatformsMatchesAll(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $cb = function () {};
        $artifact->setBinaryExtractCallback($cb, []);

        $this->assertSame($cb, $artifact->getBinaryExtractCallback());
    }

    public function testHasBinaryExtractCallbackReturnsFalseWhenNotSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->assertFalse($artifact->hasBinaryExtractCallback());
    }

    public function testHasBinaryExtractCallbackReturnsTrueWhenSet(): void
    {
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $artifact->setBinaryExtractCallback(function () {});

        $this->assertTrue($artifact->hasBinaryExtractCallback());
    }

    // ==================== After-extract callbacks ====================

    public function testEmitAfterSourceExtractCallsAllCallbacks(): void
    {
        ApplicationContext::initialize();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $called1 = false;
        $called2 = false;
        $artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called1) {
            $called1 = true;
        });
        $artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called2) {
            $called2 = true;
        });

        $artifact->emitAfterSourceExtract('/tmp/test-path');

        $this->assertTrue($called1);
        $this->assertTrue($called2);
    }

    public function testEmitAfterBinaryExtractCallsCallbackMatchingPlatform(): void
    {
        ApplicationContext::initialize();
        $platform = $this->getCurrentPlatform();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $called = false;
        $artifact->addAfterBinaryExtractCallback(function (string $target_path, string $platform) use (&$called) {
            $called = true;
        }, [$platform]);

        $artifact->emitAfterBinaryExtract('/tmp/test-path', $platform);

        $this->assertTrue($called);
    }

    public function testEmitAfterBinaryExtractSkipsCallbackForNonMatchingPlatform(): void
    {
        ApplicationContext::initialize();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $called = false;
        $artifact->addAfterBinaryExtractCallback(function () use (&$called) {
            $called = true;
        }, ['windows-x86_64']);

        $artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64');

        $this->assertFalse($called);
    }

    public function testEmitAfterBinaryExtractWithEmptyPlatformsCallsForAnyPlatform(): void
    {
        ApplicationContext::initialize();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $called = false;
        $artifact->addAfterBinaryExtractCallback(function () use (&$called) {
            $called = true;
        }, []);

        $artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64');

        $this->assertTrue($called);
    }

    // ==================== isSourceDownloaded / isBinaryDownloaded delegation ====================

    public function testIsSourceDownloadedDelegatesToArtifactCache(): void
    {
        $cache = $this->createMock(ArtifactCache::class);
        $cache->expects($this->once())
            ->method('isSourceDownloaded')
            ->with('my-pkg', false)
            ->willReturn(true);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $this->assertTrue($artifact->isSourceDownloaded());
    }

    public function testIsBinaryDownloadedDelegatesToArtifactCache(): void
    {
        $platform = $this->getCurrentPlatform();
        $cache = $this->createMock(ArtifactCache::class);
        $cache->expects($this->once())
            ->method('isBinaryDownloaded')
            ->with('my-pkg', $platform, false)
            ->willReturn(true);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $this->assertTrue($artifact->isBinaryDownloaded($platform));
    }

    // ==================== shouldUseBinary ====================

    public function testShouldUseBinaryReturnsFalseWhenNotDownloaded(): void
    {
        $platform = $this->getCurrentPlatform();
        $cache = $this->createMock(ArtifactCache::class);
        $cache->method('isBinaryDownloaded')->willReturn(false);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
        ]);
        $this->assertFalse($artifact->shouldUseBinary());
    }

    public function testShouldUseBinaryReturnsFalseWhenNoBinaryConfig(): void
    {
        $cache = $this->createMock(ArtifactCache::class);
        $cache->method('isBinaryDownloaded')->willReturn(true);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
        $this->assertFalse($artifact->shouldUseBinary());
    }

    public function testShouldUseBinaryReturnsTrueWhenDownloadedAndHasBinaryConfig(): void
    {
        $platform = $this->getCurrentPlatform();
        $cache = $this->createMock(ArtifactCache::class);
        $cache->method('isBinaryDownloaded')->willReturn(true);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        $artifact = new Artifact('my-pkg', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
            'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
        ]);
        $this->assertTrue($artifact->shouldUseBinary());
    }

    // ==================== isSourceExtracted ====================

    public function testIsSourceExtractedReturnsFalseWhenDirNotExists(): void
    {
        $cache = $this->createMock(ArtifactCache::class);
        $cache->method('getSourceInfo')->willReturn(null);

        ApplicationContext::initialize();
        ApplicationContext::set(ArtifactCache::class, $cache);

        // Use an artifact whose source dir doesn't exist on disk
        $artifact = new Artifact('this-pkg-does-not-exist-on-disk-2xyz', [
            'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
        ]);
        $this->assertFalse($artifact->isSourceExtracted());
    }

    // ==================== emitCustomBinary ====================

    public function testEmitCustomBinaryThrowsWhenNoBinaryCallbackDefined(): void
    {
        ApplicationContext::initialize();
        $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);

        $this->expectException(SPCInternalException::class);
        $artifact->emitCustomBinary();
    }

    // ==================== Helpers ====================

    private function getCurrentPlatform(): string
    {
        $emulated = getenv('EMULATE_PLATFORM');
        if ($emulated !== false) {
            return $emulated;
        }
        $os = match (PHP_OS_FAMILY) {
            'Darwin' => 'macos',
            'Windows' => 'windows',
            default => 'linux',
        };
        return "{$os}-" . arch2gnu(php_uname('m'));
    }

    private function injectArtifactConfig(string $name, array $config): void
    {
        $reflection = new \ReflectionClass(ArtifactConfig::class);
        $property = $reflection->getProperty('artifact_configs');
        $property->setAccessible(true);
        $configs = $property->getValue(null) ?? [];
        $configs[$name] = $config;
        $property->setValue(null, $configs);
    }

    /**
     * Create a stub ArtifactCache that always returns null for source/binary info
     * and delegates isSourceDownloaded/isBinaryDownloaded to return false.
     */
    private function makeStubbedArtifactCache(array $sourceInfoMap): ArtifactCache
    {
        $cacheFile = $this->tempDir . '/test-cache.json';
        file_put_contents($cacheFile, json_encode([]));
        return new ArtifactCache($cacheFile);
    }

    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        $files = array_diff(scandir($dir), ['.', '..']);
        foreach ($files as $file) {
            $path = $dir . '/' . $file;
            if (is_dir($path)) {
                $this->removeDirectory($path);
            } else {
                unlink($path);
            }
        }
        rmdir($dir);
    }
}
