<?php

/**
 * ImportOutputPngEdgeCasesTest.php
 *
 * @since     2026-05-21
 * @category  Library
 * @package   PdfImage
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-image
 *
 * This file is part of tc-lib-pdf-image software library.
 */

declare(strict_types=1);

namespace Test;

require_once __DIR__ . '/ImportProtectedMethodsHarness.php';
require_once __DIR__ . '/PngChunkParsingHarness.php';

/**
 * Extra branch-focused tests for import, output and PNG chunk parsing.
 *
 * @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
 * @phpstan-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
 */
class ImportOutputPngEdgeCasesTest extends TestUtil
{
    /**
     * @return ImageBaseData
     */
    protected function getBaseData(string $key = 'test'): array
    {
        return [
            'bits' => 8,
            'channels' => 3,
            'colspace' => 'DeviceRGB',
            'data' => 'abc',
            'exturl' => false,
            'file' => '',
            'filter' => 'FlateDecode',
            'height' => 1,
            'icc' => '',
            'ismask' => false,
            'key' => $key,
            'mapto' => IMAGETYPE_PNG,
            'native' => true,
            'obj' => 0,
            'obj_alt' => 0,
            'obj_icc' => 0,
            'obj_pal' => 0,
            'pal' => '',
            'parms' => '',
            'raw' => 'raw',
            'recode' => false,
            'recoded' => false,
            'splitalpha' => false,
            'trns' => [],
            'type' => IMAGETYPE_PNG,
            'width' => 1,
        ];
    }

    /**
     * @return ImageRawData
     */
    protected function getRawData(string $key = 'test'): array
    {
        return $this->getBaseData($key);
    }

    protected function getImportHarness(bool $pdfa = false): ImportProtectedMethodsHarness
    {
        return new ImportProtectedMethodsHarness(
            0.75,
            $this->getTestEncrypt(),
            $this->getTestFileHelper(),
            $pdfa,
            false,
        );
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    public function testGetDataThrowsWhenImageIsNotNative(): void
    {
        $this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);

        $import = $this->getImportHarness();
        $data = $this->getRawData();
        $data['native'] = false;
        $import->callGetData($data, 10, 10, 90);
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    public function testGetDataThrowsWhenNativeTypeIsUnknown(): void
    {
        $this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);

        $import = $this->getImportHarness();
        $data = $this->getRawData();
        $data['type'] = IMAGETYPE_GIF;
        $import->callGetData($data, 10, 10, 90);
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    public function testGetResizedRawDataRejectsInvalidRawImage(): void
    {
        $this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);

        $import = $this->getImportHarness();
        $data = $this->getBaseData();
        $data['raw'] = 'not-image-binary';

        \set_error_handler(static fn(): bool => true);
        try {
            $import->callGetResizedRawData($data, 10, 10, true, 90);
        } finally {
            \restore_error_handler();
        }
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    public function testGetAlphaChannelRawDataRejectsInvalidRawImage(): void
    {
        $this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);

        $import = $this->getImportHarness();
        $data = $this->getBaseData();
        $data['raw'] = 'not-image-binary';

        \set_error_handler(static fn(): bool => true);
        try {
            $import->callGetAlphaChannelRawData($data);
        } finally {
            \restore_error_handler();
        }
    }

    /**
     * @throws \Com\Tecnick\Pdf\Encrypt\Exception
     */
    public function testGetOutImageSupportsExternalStreamWithFilter(): void
    {
        $import = $this->getImportHarness();
        $import->setPon(10);

        $img = [
            'iid' => 1,
            'key' => 'ext',
            'width' => 10,
            'height' => 5,
            'defprint' => false,
            'altimgs' => [],
        ];
        $data = $this->getRawData('ext');
        $data['exturl'] = true;
        $data['filter'] = 'ASCIIHexDecode';
        $data['width'] = 10;
        $data['height'] = 5;

        $import->setCacheEntry('ext', $data);
        $out = $import->callGetOutImage($img, $data);

        $this->assertStringContainsString('/Length 0 /F << /FS /URL /F (true) >>', $out);
        $this->assertStringContainsString('/FFilter /ASCIIHexDecode', $out);
    }

    public function testGetXobjectDictByKeysFallsBackToPlainAndMaskEntries(): void
    {
        $import = $this->getImportHarness();
        $import->setXobjdict([
            'IMGplain2' => 42,
            'IMGmask3' => 43,
        ]);

        $out = $import->getXobjectDictByKeys([2, 3]);
        $this->assertSame(' /IMGplain2 42 0 R /IMGmask3 43 0 R', $out);
    }

    public function testGetOutAltImagesSkipsUnknownOrUnbuiltAlternateImages(): void
    {
        $import = $this->getImportHarness();
        $import->setPon(20);

        $import->setImageEntry(2, [
            'iid' => 2,
            'key' => 'alt-no-object',
            'width' => 10,
            'height' => 5,
            'defprint' => true,
            'altimgs' => [],
        ]);

        $altData = $this->getRawData('alt-no-object');
        $altData['obj'] = 0;
        $import->setCacheEntry('alt-no-object', $altData);

        $img = [
            'iid' => 1,
            'key' => 'main',
            'width' => 10,
            'height' => 5,
            'defprint' => false,
            'altimgs' => [999, 2],
        ];
        $data = $this->getRawData('main');

        $out = $import->callGetOutAltImages($img, $data);

        $this->assertStringContainsString('21 0 obj', $out);
        $this->assertStringContainsString('[ ]', $out);
    }

    public function testGetOutTransparencyIndexedSkipsNonZeroValues(): void
    {
        $import = $this->getImportHarness();
        $data = $this->getRawData();
        $data['colspace'] = 'Indexed';
        $data['trns'] = [0 => 0, 1 => 9, 2 => 0];

        // Indexed: the fully-transparent palette indices (alpha 0) are masked.
        $out = $import->callGetOutTransparency($data);
        $this->assertSame('0 0 2 2 ', $out);
    }

    public function testGetOutTransparencyRgbUsesColorValues(): void
    {
        $import = $this->getImportHarness();
        $data = $this->getRawData();
        $data['colspace'] = 'DeviceRGB';
        // trns holds the transparent colour sample values (R, G, B).
        $data['trns'] = [255, 128, 0];

        $out = $import->callGetOutTransparency($data);
        $this->assertSame('255 255 128 128 0 0 ', $out);
    }

    public function testGetOutTransparencyGrayUsesColorValue(): void
    {
        $import = $this->getImportHarness();
        $data = $this->getRawData();
        $data['colspace'] = 'DeviceGray';
        $data['trns'] = [128];

        $out = $import->callGetOutTransparency($data);
        $this->assertSame('128 128 ', $out);
    }

    public function testGetTrnsChunkHandlesGrayAndRgbFormats(): void
    {
        $png = new PngChunkParsingHarness();

        $gray = $this->getBaseData();
        $gray['colspace'] = 'DeviceGray';
        $gray['raw'] = "\x00\x7f";
        $gray['trns'] = [];
        $grayOffset = 0;
        $gray = $png->callGetTrnsChunk($gray, $grayOffset, 2);
        $this->assertSame([127], $gray['trns']);
        $this->assertSame(6, $grayOffset);

        $rgb = $this->getBaseData();
        $rgb['colspace'] = 'DeviceRGB';
        $rgb['raw'] = "\x00\x11\x00\x22\x00\x33";
        $rgb['trns'] = [];
        $rgbOffset = 0;
        $rgb = $png->callGetTrnsChunk($rgb, $rgbOffset, 6);
        $this->assertSame([17, 34, 51], $rgb['trns']);
        $this->assertSame(10, $rgbOffset);
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \RangeException
     */
    public function testGetIccpChunkThrowsOnInvalidCompressedProfile(): void
    {
        $this->bcExpectException(\Com\Tecnick\Pdf\Image\Exception::class);

        $png = new PngChunkParsingHarness();
        $data = $this->getBaseData();
        $data['raw'] = "ICC\x00\x00BAD";
        $offset = 0;
        $byte = new \Com\Tecnick\File\Byte($data['raw']);

        \set_error_handler(static fn(): bool => true);
        try {
            $png->callGetIccpChunk($byte, $data, $offset, 8);
        } finally {
            \restore_error_handler();
        }
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    public function testGetResizedRawDataPreservesIndexedTransparency(): void
    {
        $import = $this->getImportHarness();

        $raw = \file_get_contents(__DIR__ . '/images/200x100_INDEXALPHA.png');
        $this->assertIsString($raw);

        $data = $this->getBaseData();
        $data['raw'] = $raw;
        $data['width'] = 200;
        $data['height'] = 100;

        // alpha=false exercises the indexed-palette transparency branch.
        \set_error_handler(static fn(): bool => true);
        try {
            $resized = $import->callGetResizedRawData($data, 100, 50, false, 90);
        } finally {
            \restore_error_handler();
        }

        $this->assertSame(100, $resized['width']);
        $this->assertSame(50, $resized['height']);
        $this->assertTrue($resized['recoded']);
    }

    /**
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Encrypt\Exception
     */
    public function testGetImageDimensionsByKeyFitWithZeroSourceReturnsBox(): void
    {
        $import = $this->getImportHarness();

        $data = $this->getRawData('zero-source');
        $data['width'] = 0;
        $data['height'] = 0;
        $import->setCacheEntry('zero-source', $data);

        // A zero-sized source falls back to the requested bounding box as-is.
        $this->assertSame(
            ['width' => 80, 'height' => 80],
            $import->getImageDimensionsByKey('zero-source', 80, 80, true),
        );
    }
}
