<?php

namespace Tests\Unit\Dashboard;

use App\Services\CampaignPerformance\CampaignPerformanceTablePresenter;
use App\Services\Dashboard\DashboardTablePresenter;
use App\Services\Dashboard\KpiRegistry;
use PHPUnit\Framework\TestCase;

/**
 * The aggregate ("Total") row's cell list.
 *
 * Two properties matter, and neither is visible from reading the views:
 *
 *  1. The cell count equals the column count. Every table this feeds renders the
 *     row as a <tfoot> with one <th> per column and no colspan, because the
 *     AJAX-driven tables address footer cells by index. A short or long list
 *     shifts the footer against the header and makes DataTables warn on init.
 *  2. Only the base metrics carry a value. A summed ratio is not a ratio of sums,
 *     so CTR / eCPC / CPC / eCPM / CVR / CPA / COS / ROAS must stay blank rather
 *     than show an arithmetic mean of the rows above — which is what filling them
 *     from the same $totals array would silently produce.
 *
 * Plain TestCase, matching the rest of tests/unit: buildTotalsCells() is a pure
 * static, and the bootstrap stubs session() so format_money() falls back to '$'
 * at a rate of 1.0. The helper is a function file, so it needs requiring
 * explicitly — the bootstrap's PSR-4 autoloader only covers App\ classes.
 */
final class TotalsRowTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        require_once dirname(__DIR__, 3) . '/app/Helpers/utils_helper.php';
    }

    /** @return array<string,float> */
    private function sums(): array
    {
        return [
            'impressions' => 5764480.0,
            'clicks'      => 100017.0,
            'spend'       => 29601.815,
            'conversions' => 1204.0,
        ];
    }

    // ---------------------------------------------------------------------
    // Cell count
    // ---------------------------------------------------------------------

    public function testCellCountIsLeadingCellsPlusOnePerMetric(): void
    {
        $metrics = KpiRegistry::metrics();
        $cells   = KpiRegistry::buildTotalsCells($metrics, $this->sums());

        $this->assertCount(1 + count($metrics), $cells);
    }

    public function testExtraLeadingCellsAreCounted(): void
    {
        $metrics = KpiRegistry::metrics();
        $cells   = KpiRegistry::buildTotalsCells($metrics, $this->sums(), ['Total', '']);

        $this->assertCount(2 + count($metrics), $cells);
        $this->assertSame('Total', $cells[0]);
        $this->assertSame('', $cells[1]);
    }

    /**
     * The two tables that render this server-side, each against its OWN metric
     * list. They no longer agree: the Dashboard filters ADVERTISER_METRIC_IDS out
     * (advertiser-reported figures are not the dashboard's story), Campaign
     * Performance keeps them and pairs each with its estimated counterpart. So
     * the counts are deliberately different, and each has to be checked through
     * the presenter that actually feeds the view.
     *
     * These numbers are hardcoded on purpose. testCellCountIsLeadingCellsPlusOnePerMetric
     * already pins the derived `1 + count($metrics)` relationship; the point of
     * repeating it as literals is to fail loudly when the registry gains or loses
     * a metric, so that change is a decision rather than a surprise. Reading them
     * from count() would make this method a restatement of that one.
     */
    public function testCellCountMatchesTheDashboardAndCampaignPerformanceColumnCounts(): void
    {
        $this->assertCount(
            14,
            KpiRegistry::buildTotalsCells(DashboardTablePresenter::tableMetrics(), $this->sums()),
            'Dashboard: Campaign + 13 metrics (the two advertiser metrics are filtered out)'
        );

        $this->assertCount(
            16,
            KpiRegistry::buildTotalsCells(CampaignPerformanceTablePresenter::tableMetrics(), $this->sums()),
            'Campaign Performance: Campaign + all 15 metrics, reordered'
        );
    }

    /**
     * The reason the two counts above differ. Asserted separately so a future
     * registry change reports which grid's contract moved, rather than just
     * "expected 14, got 16" on a cell count.
     */
    public function testOnlyCampaignPerformanceCarriesTheAdvertiserMetrics(): void
    {
        $dashboard = array_column(DashboardTablePresenter::tableMetrics(), 'id');
        $cp        = array_column(CampaignPerformanceTablePresenter::tableMetrics(), 'id');

        foreach (KpiRegistry::ADVERTISER_METRIC_IDS as $id) {
            $this->assertNotContains($id, $dashboard, $id . ' must not reach the dashboard grid');
            $this->assertContains($id, $cp, $id . ' must stay on Campaign Performance');
        }

        // Set difference in both directions: the advertiser metrics are the only
        // thing CP has that the dashboard does not, and the dashboard has nothing
        // CP lacks. Ordering is deliberately ignored -- CP reorders via
        // TRAILING_METRIC_IDS, which CampaignPerformanceTableColumnOrderTest owns.
        $onlyOnCp = array_values(array_diff($cp, $dashboard));
        sort($onlyOnCp);
        $advertiser = KpiRegistry::ADVERTISER_METRIC_IDS;
        sort($advertiser);

        $this->assertSame(
            $advertiser,
            $onlyOnCp,
            'CP and the dashboard must differ by exactly the advertiser metrics'
        );
        $this->assertSame(
            [],
            array_values(array_diff($dashboard, $cp)),
            'the dashboard shows a metric Campaign Performance does not'
        );
    }

    // ---------------------------------------------------------------------
    // Which cells carry a value
    // ---------------------------------------------------------------------

    public function testOnlyTheBaseMetricsAreFilled(): void
    {
        $metrics = KpiRegistry::metrics();
        $cells   = KpiRegistry::buildTotalsCells($metrics, $this->sums());

        foreach ($metrics as $i => $metric) {
            $cell     = $cells[$i + 1];
            $totalled = in_array($metric['id'], KpiRegistry::TOTALED_METRIC_IDS, true);

            if ($totalled) {
                $this->assertNotSame('', $cell, $metric['id'] . ' should be totalled');
            } else {
                $this->assertSame('', $cell, $metric['id'] . ' must stay blank — a summed ratio is not a ratio of sums');
            }
        }
    }

    public function testDerivedRatioColumnsAreBlankEvenWhenSuppliedInTotals(): void
    {
        // A caller passing an expanded totals array (KpiRegistry::expandTotals()
        // output) must not get the ratios rendered — they are per-row values there.
        $expanded = KpiRegistry::expandTotals($this->sums());
        $cells    = KpiRegistry::buildTotalsCells(KpiRegistry::metrics(), $expanded);

        $byId = [];
        foreach (KpiRegistry::metrics() as $i => $metric) {
            $byId[$metric['id']] = $cells[$i + 1];
        }

        foreach (['ctr', 'ecpc', 'cpc', 'ecpm', 'cvr', 'cpa', 'cos', 'roas', 'est_revenue'] as $id) {
            $this->assertSame('', $byId[$id], $id . ' must stay blank');
        }
    }

    // ---------------------------------------------------------------------
    // Formatting
    // ---------------------------------------------------------------------

    public function testValuesUseTheSameFormatterAsTheBodyRows(): void
    {
        $sums  = $this->sums();
        $cells = KpiRegistry::buildTotalsCells(KpiRegistry::metrics(), $sums);

        $byId = [];
        foreach (KpiRegistry::metrics() as $i => $metric) {
            $byId[$metric['id']] = $cells[$i + 1];
        }

        foreach (KpiRegistry::TOTALED_METRIC_IDS as $id) {
            $this->assertSame(KpiRegistry::formatNumber($id, $sums[$id]), $byId[$id], $id);
        }

        // Spot-check the rendered strings so a formatter change is visible here.
        $this->assertSame('5,764,480', $byId['impressions']);
        $this->assertSame('100,017', $byId['clicks']);
        $this->assertSame('$29,601.82', $byId['spend']);
        $this->assertSame('1,204', $byId['conversions']);
    }

    public function testMissingMetricReadsAsZeroRatherThanBlank(): void
    {
        // A table whose data source has no conversions column still gets a 0 in that
        // cell, not an empty one: blank in a totalled column reads as "not summed".
        $cells = KpiRegistry::buildTotalsCells(KpiRegistry::metrics(), ['clicks' => 5.0]);

        $byId = [];
        foreach (KpiRegistry::metrics() as $i => $metric) {
            $byId[$metric['id']] = $cells[$i + 1];
        }

        $this->assertSame('5', $byId['clicks']);
        $this->assertSame('0', $byId['conversions']);
        $this->assertSame('$0.00', $byId['spend']);
    }
}
