<?php

namespace MintyPHP\Tests\Service\Export;

use MintyPHP\Service\Export\CsvExportService;
use MintyPHP\Service\Export\ExportColumn;
use PHPUnit\Framework\TestCase;

class CsvExportServiceTest extends TestCase
{
    private CsvExportService $service;

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

    public function testCsvFlavorUsesCommaWithoutBom(): void
    {
        $output = $this->render(
            rows: [
                ['name' => 'Alice', 'city' => 'Berlin'],
                ['name' => 'Bob', 'city' => 'Hamburg'],
            ],
            columns: [
                new ExportColumn('Name', static fn (array $r): string => (string) $r['name']),
                new ExportColumn('City', static fn (array $r): string => (string) $r['city']),
            ],
            flavor: CsvExportService::FLAVOR_CSV,
        );

        $this->assertStringStartsWith('Name,City', $output);
        $this->assertStringContainsString("Alice,Berlin", $output);
        $this->assertStringContainsString("Bob,Hamburg", $output);
        $this->assertStringNotContainsString("\xEF\xBB\xBF", $output, 'CSV flavor must not emit UTF-8 BOM');
    }

    public function testExcelFlavorUsesSemicolonWithBom(): void
    {
        $output = $this->render(
            rows: [['name' => 'Alice', 'city' => 'Berlin']],
            columns: [
                new ExportColumn('Name', static fn (array $r): string => (string) $r['name']),
                new ExportColumn('City', static fn (array $r): string => (string) $r['city']),
            ],
            flavor: CsvExportService::FLAVOR_EXCEL,
        );

        $this->assertStringStartsWith("\xEF\xBB\xBF", $output, 'Excel flavor must start with UTF-8 BOM');
        $this->assertStringContainsString('Name;City', $output);
        $this->assertStringContainsString('Alice;Berlin', $output);
    }

    public function testFormulaInjectionIsEscaped(): void
    {
        foreach (['=SUM(A1:A10)', '@foo()', '+cmd', '-bad', "\tTAB", "\rCR"] as $payload) {
            $this->assertSame(
                "'" . $payload,
                CsvExportService::escapeValue($payload),
                'Value starting with leading special character must be quoted: ' . bin2hex($payload)
            );
        }
    }

    public function testHeaderRowIsAlsoEscaped(): void
    {
        $output = $this->render(
            rows: [['v' => 'safe']],
            columns: [new ExportColumn('=HYPERLINK("x")', static fn (array $r): string => (string) $r['v'])],
        );

        // The header itself must be escaped — a future consumer may pass user-provided column labels.
        $this->assertStringContainsString('"\'=HYPERLINK(""x"")"', $output);
    }

    public function testSignedNumericIsAllowedForPhoneLikeColumns(): void
    {
        $this->assertSame('+49 123 456', CsvExportService::escapeValue('+49 123 456', allowSignedNumeric: true));
        $this->assertSame('-12.5', CsvExportService::escapeValue('-12.5', allowSignedNumeric: true));
        // Non-numeric leading + still gets escaped even when flag set
        $this->assertSame("'+SUM(1)", CsvExportService::escapeValue('+SUM(1)', allowSignedNumeric: true));
    }

    public function testNullAndBoolsAreNormalized(): void
    {
        $this->assertSame('', CsvExportService::escapeValue(null));
        $this->assertSame('1', CsvExportService::escapeValue(true));
        $this->assertSame('0', CsvExportService::escapeValue(false));
        $this->assertSame('', CsvExportService::escapeValue(''));
    }

    public function testSpecialCharactersAreQuotedProperly(): void
    {
        $output = $this->render(
            rows: [['value' => 'contains "quotes" and, commas']],
            columns: [new ExportColumn('V', static fn (array $r): string => (string) $r['value'])],
        );

        $this->assertStringContainsString('"contains ""quotes"" and, commas"', $output);
    }

    public function testMultilineValuesAreQuoted(): void
    {
        $output = $this->render(
            rows: [['note' => "line 1\nline 2"]],
            columns: [new ExportColumn('Note', static fn (array $r): string => (string) $r['note'])],
        );

        $this->assertStringContainsString("\"line 1\nline 2\"", $output);
    }

    public function testEmptyColumnsThrow(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->render(rows: [['a' => 'b']], columns: []);
    }

    public function testIterableRowsSupported(): void
    {
        $generator = (function () {
            yield ['n' => 1];
            yield ['n' => 2];
        })();

        $output = $this->render(
            rows: $generator,
            columns: [new ExportColumn('N', static fn (array $r): string => (string) $r['n'])],
        );

        $this->assertStringContainsString("N\n1\n2", $output);
    }

    /**
     * @param iterable<array<string,mixed>> $rows
     * @param list<ExportColumn> $columns
     */
    private function render(iterable $rows, array $columns, string $flavor = CsvExportService::FLAVOR_CSV): string
    {
        $handle = fopen('php://memory', 'w+');
        $this->service->writeTo($handle, $rows, $columns, $flavor);
        rewind($handle);
        $contents = stream_get_contents($handle);
        fclose($handle);
        return (string) $contents;
    }
}
