<?php

namespace Tests\Feature;

use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\SiteURI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
use Config\Services;
use PHPUnit\Framework\Attributes\DataProvider;

/**
 * Renders the Import results lists the way the page does.
 *
 * The reported raw JSON was produced in the view, not in the service, so every
 * service-level assertion still passes with the json_encode() call put back.
 * These render the partials instead, which is the only level that can see it.
 */
final class ImportResultsRenderTest extends CIUnitTestCase
{
    private int $obLevel = 0;

    // view() leaves a buffer open here, as it does in MasterScopingTest; same
    // guard, so a render failure cannot swallow the rest of the suite's output.
    protected function setUp(): void
    {
        parent::setUp();
        $this->obLevel = ob_get_level();
    }

    protected function tearDown(): void
    {
        while (ob_get_level() > $this->obLevel) {
            ob_end_clean();
        }
        parent::tearDown();
    }

    public static function tabs(): array
    {
        return [['campaigns'], ['creatives']];
    }

    private function render(string $tab, array $response): string
    {
        // The controller autoloads these; rendering a partial directly does not.
        helper(['utils', 'auth']);

        // baseURL() resolves through the current request, which only exists once
        // something has routed. Give the view one.
        $config = config(App::class);
        Services::injectMock('request', new IncomingRequest(
            $config,
            new SiteURI($config, 'import-export/import'),
            null,
            new UserAgent()
        ));

        return view('import-export/partials/tab_' . $tab, [
            'activeTab' => $tab,
            'responses' => [$tab => [$response]],
            'campaigns' => [],
            'dmaList'   => [],
        ]);
    }

    #[DataProvider('tabs')]
    public function testAnArrayMessageNeverRendersAsJson(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'status'  => false,
            'message' => [['field' => 'status', 'message' => 'The Status field must be one of: A,S.']],
            'errors'  => [],
        ]);

        // esc() would render a JSON payload with escaped quotes, so check both.
        $this->assertStringNotContainsString('{"', $html);
        $this->assertStringNotContainsString('{&quot;', $html);
        $this->assertStringContainsString('Field-status', $html);
    }

    #[DataProvider('tabs')]
    public function testANestedErrorMessageDoesNotRenderAsTheWordArray(string $tab): void
    {
        // A nested value in the sub-list used to be cast straight to string.
        // In CI4 that warning is a thrown ErrorException, so this would not
        // merely print "Array" — it would 500 the page and lose the batch.
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'status'  => false,
            'message' => '1 problem to fix (CSV row 2):',
            'errors'  => [
                ['field' => 'status', 'message' => [['field' => 'status', 'message' => 'Must be one of: A,S.']]],
            ],
        ]);

        $this->assertStringNotContainsString('Array', $html);
        $this->assertStringContainsString('Must be one of: A,S.', $html);
    }

    #[DataProvider('tabs')]
    public function testTheReadableValidationMessageAndItsColumnBothRender(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'status'  => false,
            'message' => '1 problem to fix (CSV row 2):',
            'errors'  => [['field' => 'status', 'message' => 'The Status field must be one of: A,S.']],
        ]);

        $this->assertStringContainsString('1 problem to fix (CSV row 2):', $html);
        $this->assertStringContainsString('status: The Status field must be one of: A,S.', $html);
        // Not a bare '{' — the partial's CSV-template script legitimately has one.
        $this->assertStringNotContainsString('{"', $html);
        $this->assertStringNotContainsString('{&quot;', $html);
    }

    #[DataProvider('tabs')]
    public function testASuccessLineNamesItsCsvRow(string $tab): void
    {
        // QA read a success plus an unrelated failure as one contradictory
        // result, because the green line named no row at all.
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'Summer',
            'row'     => 2,
            'status'  => true,
            'message' => 'Campaign has been added — new record, ID:12345',
            'errors'  => [],
        ]);

        $this->assertStringContainsString('Row 2 — &quot;Summer&quot;:', $html);
    }

    #[DataProvider('tabs')]
    public function testAFailureLineIsNotGivenASecondRowNumber(string $tab): void
    {
        // Failures already carry their row, so prefixing them would read
        // "Row 3 — ... (CSV row 3)".
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'Winter',
            'row'     => 3,
            'status'  => false,
            'message' => 'field type is required (CSV row 3)',
            'errors'  => [],
        ]);

        $this->assertStringNotContainsString('Row 3 —', $html);
        $this->assertStringContainsString('(CSV row 3)', $html);
    }

    public function testAlphabeticBidAndBudgetRenderAsReadableBullets(): void
    {
        $html = $this->render('campaigns', [
            'type'    => 'post',
            'name'    => 'QA Alpha',
            'row'     => 2,
            'status'  => false,
            'message' => '2 problems to fix (CSV row 2):',
            'errors'  => [
                ['field' => 'budget', 'message' => 'The Daily Budget field must contain a number greater than or equal to 2.'],
                ['field' => 'bid', 'message' => 'Rate value must be in the range: 0.001 - 100'],
            ],
        ]);

        $this->assertStringContainsString('budget: The Daily Budget field must contain a number greater than or equal to 2.', $html);
        $this->assertStringContainsString('bid: Rate value must be in the range: 0.001 - 100', $html);
        $this->assertStringNotContainsString('{"', $html);
        $this->assertStringNotContainsString('{&quot;', $html);
    }

    #[DataProvider('tabs')]
    public function testAStringErrorAppearsExactlyOnce(string $tab): void
    {
        // QA: "displays the same validation message twice". The message line and the
        // errors sub-list both carried it.
        $reason = 'Rank should be blank if parent_creative_id is not present !!';
        $html   = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'row'     => 2,
            'status'  => false,
            'message' => $reason . ' (CSV row 2)',
            'errors'  => $reason,
        ]);

        $this->assertSame(1, substr_count($html, esc($reason)), 'the reason must appear once');
    }

    #[DataProvider('tabs')]
    public function testAPerFieldErrorListRendersItsBullets(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'row'     => 2,
            'status'  => false,
            'message' => '1 problem to fix (CSV row 2):',
            'errors'  => [['field' => 'status', 'message' => 'The Status field must be one of: A,S.']],
        ]);

        $this->assertStringContainsString('1 problem to fix (CSV row 2):', $html);
        $this->assertStringContainsString('status: The Status field must be one of: A,S.', $html);
    }

    #[DataProvider('tabs')]
    public function testAnEmptyErrorEntryRendersNoBullet(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'post',
            'name'    => 'QA Row',
            'row'     => 2,
            'status'  => false,
            'message' => 'request failed with HTTP 422 (CSV row 2).',
            'errors'  => [['field' => '', 'message' => '']],
        ]);

        $this->assertStringNotContainsString('<ul class="small">', $html);
    }

    #[DataProvider('tabs')]
    public function testAHeaderWarningRendersAsAWarningRatherThanASuccess(string $tab): void
    {
        // status is true — the file was accepted — so a colour taken from status
        // alone would paint this green and read as "all good".
        $html = $this->render($tab, [
            'type'    => 'warning',
            'status'  => true,
            'message' => 'Ignored 1 column the importer does not recognise: targeting_frequancy.',
        ]);

        $this->assertStringContainsString('text-warning', $html);
        $this->assertStringNotContainsString('text-success', $html);
        $this->assertStringContainsString('targeting_frequancy', $html);
    }

    #[DataProvider('tabs')]
    public function testDroppedCellsRenderUnderTheRowTheyCameFrom(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'patch',
            'id'      => 77,
            'row'     => 2,
            'status'  => true,
            'message' => 'Campaign has been updated',
            'ignored' => [
                ['column' => 'targeting_frequency', 'reason' => '"abc" is not a number'],
            ],
        ]);

        $this->assertStringContainsString('Campaign has been updated', $html);
        $this->assertStringContainsString('Ignored 1 cell: targeting_frequency (&quot;abc&quot; is not a number)', $html);
    }

    #[DataProvider('tabs')]
    public function testARowWithNoDroppedCellsRendersNoIgnoredLine(string $tab): void
    {
        $html = $this->render($tab, [
            'type'    => 'patch',
            'id'      => 77,
            'row'     => 2,
            'status'  => true,
            'message' => 'Campaign has been updated',
            'ignored' => [],
        ]);

        // Not the bare word: the field guide below legitimately marks read-only
        // columns "Ignored".
        $this->assertStringNotContainsString('Ignored 1 cell', $html);
        $this->assertStringNotContainsString('cells:', $html);
    }
}
