<?php

namespace Tests\Unit;

use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Support\QrCode;
use PHPUnit\Framework\TestCase;

/**
 * A broken QR encoder does not look broken.
 *
 * Every bug found while writing this class produced a perfectly plausible
 * square of black and white that no scanner would read: a Reed-Solomon
 * generator polynomial built with its terms reversed, and missing
 * version-information blocks for versions 7 and up. Neither is visible by
 * looking at the output, and neither throws.
 *
 * So the encoder was verified module-for-module against an independent
 * reference implementation — versions 1 through 10, all eight masks, given
 * identical codewords — and these fixtures are that verification frozen.
 */
class QrCodeTest extends TestCase
{
    /** @return array<int, array{data: string, version: int, mask: int, matrix: array<int, string>}> */
    private function golden(): array
    {
        return require __DIR__ . '/../Fixtures/qr_golden.php';
    }

    public function test_the_matrix_matches_the_verified_reference(): void
    {
        foreach ($this->golden() as $case) {
            $produced = array_map(
                fn (array $row) => implode('', array_map(fn (bool $dark) => $dark ? '1' : '0', $row)),
                QrCode::matrix($case['data'])
            );

            $this->assertSame(
                $case['matrix'],
                $produced,
                "version {$case['version']} mask {$case['mask']} no longer matches the reference"
            );
        }
    }

    public function test_the_symbol_is_square_and_the_right_size_for_its_version(): void
    {
        foreach ($this->golden() as $case) {
            $matrix = QrCode::matrix($case['data']);
            $expected = 17 + 4 * $case['version'];

            $this->assertCount($expected, $matrix);

            foreach ($matrix as $row) {
                $this->assertCount($expected, $row);
            }
        }
    }

    public function test_the_three_finder_patterns_are_where_a_scanner_looks_for_them(): void
    {
        $matrix = QrCode::matrix('v1.0a9a8af1-a023-43e2-956d-5728d39c0139.1.ABCDEFGH');
        $n = count($matrix);

        foreach ([[0, 0], [0, $n - 7], [$n - 7, 0]] as [$top, $left]) {
            // Outer ring dark, inner ring light, 3x3 core dark.
            $this->assertTrue($matrix[$top][$left]);
            $this->assertTrue($matrix[$top][$left + 6]);
            $this->assertFalse($matrix[$top + 1][$left + 1]);
            $this->assertTrue($matrix[$top + 3][$left + 3]);
        }
    }

    public function test_the_timing_patterns_alternate(): void
    {
        $matrix = QrCode::matrix('v1.0a9a8af1-a023-43e2-956d-5728d39c0139.1.ABCDEFGH');
        $n = count($matrix);

        for ($i = 8; $i < $n - 8; $i++) {
            $this->assertSame($i % 2 === 0, $matrix[6][$i], "horizontal timing broken at {$i}");
            $this->assertSame($i % 2 === 0, $matrix[$i][6], "vertical timing broken at {$i}");
        }
    }

    public function test_the_dark_module_is_set(): void
    {
        // One module that is dark in every valid symbol, at (4v+9, 8).
        foreach ($this->golden() as $case) {
            $matrix = QrCode::matrix($case['data']);

            $this->assertTrue($matrix[4 * $case['version'] + 9][8]);
        }
    }

    public function test_the_version_chosen_is_the_smallest_that_fits(): void
    {
        // 16 data codewords at level M in version 1, minus 2 for the mode and
        // length header, leaves room for 14 bytes.
        $this->assertCount(21, QrCode::matrix(str_repeat('a', 14)));
        $this->assertCount(25, QrCode::matrix(str_repeat('a', 15)));
    }

    public function test_a_payload_beyond_the_supported_versions_throws_rather_than_truncating(): void
    {
        $this->expectException(DomainException::class);

        QrCode::matrix(str_repeat('a', 400));
    }

    public function test_the_svg_is_self_contained_and_carries_no_external_reference(): void
    {
        $svg = QrCode::svg('v1.0a9a8af1-a023-43e2-956d-5728d39c0139.1.ABCDEFGH', 240);

        $this->assertStringStartsWith('<svg', $svg);
        $this->assertStringEndsWith('</svg>', $svg);

        // The one QR that existed in this codebase before was an <img> pointing
        // at api.qrserver.com, which sends the payload to a third party and
        // stops working when the venue's wifi does.
        // The SVG namespace URI is a namespace, not a fetch.
        $withoutNamespace = str_replace('http://www.w3.org/2000/svg', '', $svg);

        $this->assertStringNotContainsString('http://', $withoutNamespace);
        $this->assertStringNotContainsString('https://', $withoutNamespace);
        $this->assertStringNotContainsString('<script', $svg);
        $this->assertStringNotContainsString('<image', $svg);
    }

    public function test_the_svg_leaves_the_required_quiet_zone(): void
    {
        // Four light modules on every side. A scanner needs them to find the
        // symbol's edge at all.
        $svg = QrCode::svg('A', 100, 4);

        $this->assertStringContainsString('viewBox="0 0 29 29"', $svg, '21 modules plus 4 each side');
    }
}
