<?php

namespace Tests\Feature;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
use PHPUnit\Framework\Attributes\DataProvider;

/**
 * Table-driven campaign edit persistence: set / clear-with-sentinel /
 * leave-alone-without-sentinel for each modeled field.
 *
 * php8.2 -d newrelic.enabled=0 -d newrelic.daemon.dont_launch=3 \
 *   vendor/bin/phpunit --bootstrap tests/feature_bootstrap.php \
 *   tests/feature/CampaignUpdateFieldPersistenceTest.php
 */
final class CampaignUpdateFieldPersistenceTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    private const ADV = 999999992;

    private array $sess;

    /** @var \CodeIgniter\Database\BaseConnection */
    private $conn;

    /** @var list<int> */
    private array $campaignIds = [];

    protected function setUp(): void
    {
        parent::setUp();

        $this->sess = [
            'logged_in' => 1,
            'adv_id'    => self::ADV,
            'user_id'   => self::ADV,
            'username'  => 'QA Bot',
            'role'      => 'admin',
        ];

        $filters = config('Filters');
        unset($filters->filters['csrf']);

        $this->conn = \Config\Database::connect('default');
        $this->cleanup();
    }

    protected function tearDown(): void
    {
        $this->cleanup();
        parent::tearDown();
    }

    private function cleanup(): void
    {
        $camps = $this->conn->table('campaign')->select('id')
            ->where('advertiser_id', self::ADV)->get()->getResultArray();
        foreach ($camps as $r) {
            $cid = (int) $r['id'];
            $this->conn->table('campaign_targeting')->where('campaign_id', $cid)->delete();
            $this->conn->table('campaign_creative_link')->where('campaign_id', $cid)->delete();
            $this->conn->table('adv_camp_categories_mapping')->where('campaign_id', $cid)->delete();
        }
        $this->conn->table('campaign')->where('advertiser_id', self::ADV)->delete();
        $creatives = $this->conn->table('creatives')->select('id')
            ->where('aid', self::ADV)->get()->getResultArray();
        foreach ($creatives as $r) {
            $this->conn->table('campaign_creative_link')->where('creative_id', (int) $r['id'])->delete();
        }
        $this->conn->table('creatives')->where('aid', self::ADV)->delete();
        $this->campaignIds = [];
    }

    private function req()
    {
        return $this->withSession($this->sess);
    }

    /**
     * @param array<string, mixed> $json
     */
    private function insertCampaign(array $json, int $freqCap = 0, int $xmlTraffic = 0): int
    {
        $this->conn->table('campaign')->insert([
            'advertiser_id'   => self::ADV,
            'name'            => 'QA Field Persistence Campaign',
            'status'          => 'A',
            'bid'             => '0.100000',
            'budget'          => '50.00',
            'campaign_type'   => 1,
            'campaign_mode'   => 'CPM',
            'freq_cap'        => $freqCap,
            'xml_traffic'     => $xmlTraffic,
            'duration'        => 0,
            'linktrust_pixel' => '',
            'site'            => '',
            'pixel'           => '',
            'pixel_conv'      => '',
            'system'          => 0,
            'system_id'       => 0,
            'usonly'          => 0,
        ]);
        $id = (int) $this->conn->insertID();
        $this->campaignIds[] = $id;

        $json = ['affiliates' => ['120438'], 'countries' => ['USA']] + $json;
        $this->conn->table('campaign_targeting')->insert([
            'campaign_id' => $id,
            'json'        => json_encode($json),
        ]);

        return $id;
    }

    /**
     * @return array{row: array<string, mixed>, json: array<string, mixed>}
     */
    private function reload(int $id): array
    {
        $row = $this->conn->table('campaign')->where('id', $id)->get()->getRowArray();
        $targetingRow = $this->conn->table('campaign_targeting')
            ->where('campaign_id', $id)->get()->getRowArray();
        $json = json_decode((string) ($targetingRow['json'] ?? '{}'), true) ?: [];

        return ['row' => $row, 'json' => $json];
    }

    /**
     * @return array<string, mixed>
     */
    private function basePost(): array
    {
        return [
            'name'          => 'QA Field Persistence Campaign',
            'bid'           => '0.10',
            'budget'        => '50',
            'status'        => 'A',
            'campaign_type' => 'Display',
        ];
    }

    /**
     * @return array<string, array<int, array<string, mixed>>>
     */
    public static function fieldCases(): array
    {
        $scheduleSeed = [[
            'day'        => 'monday',
            'start_time' => '09:00',
            'end_time'   => '17:00',
        ]];

        return [
            'connection_types set' => [[
                'seed'   => ['connection_types' => ['wifi']],
                'post'   => [
                    'targeting-connection-types' => '0',
                    'connection-type'            => ['ethernet', 'cellular'],
                ],
                'expect' => ['connection_types' => ['ethernet', 'cellular']],
            ]],
            'connection_types clear' => [[
                'seed'    => ['connection_types' => ['wifi']],
                'post'    => [
                    'targeting-connection-types' => '1',
                    'connection-type'            => ['wifi'],
                ],
                'missing' => ['connection_types'],
            ]],
            'connection_types leave-alone' => [[
                'seed'   => ['connection_types' => ['wifi']],
                'post'   => [],
                'expect' => ['connection_types' => ['wifi']],
            ]],

            'categories set' => [[
                'seed'   => ['categories' => ['include' => ['IAB1-1']]],
                'post'   => [
                    'targeting-categories_mode' => 'exclude',
                    'categories'                => ['IAB2-1'],
                ],
                'expect' => ['categories' => ['exclude' => ['IAB2-1']]],
            ]],
            'categories clear' => [[
                'seed'    => ['categories' => ['include' => ['IAB1-1']]],
                'post'    => ['targeting-categories_mode' => 'include'],
                'missing' => ['categories'],
            ]],
            'categories leave-alone' => [[
                'seed'   => ['categories' => ['include' => ['IAB1-1']]],
                'post'   => [],
                'expect' => ['categories' => ['include' => ['IAB1-1']]],
            ]],

            'browsers set' => [[
                'seed'   => ['browsers' => ['chrome']],
                'post'   => [
                    'targeting-browsers' => '0',
                    'browsers'           => ['firefox', 'safari'],
                ],
                'expect' => ['browsers' => ['firefox', 'safari']],
            ]],
            'browsers clear' => [[
                'seed'    => ['browsers' => ['chrome']],
                'post'    => [
                    'targeting-browsers' => '1',
                    'browsers'           => ['chrome'],
                ],
                'missing' => ['browsers'],
            ]],
            'browsers leave-alone' => [[
                'seed'   => ['browsers' => ['chrome']],
                'post'   => [],
                'expect' => ['browsers' => ['chrome']],
            ]],

            'supplies set' => [[
                'seed'   => ['supplies' => ['web']],
                'post'   => ['targeting-supplies' => ['mobile_web']],
                'expect' => ['supplies' => ['mobile']],
            ]],
            'supplies clear' => [[
                'seed'    => ['supplies' => ['web']],
                'post'    => ['targeting-supplies' => ['all', 'web']],
                'missing' => ['supplies'],
            ]],
            'supplies leave-alone' => [[
                'seed'   => ['supplies' => ['web']],
                'post'   => [],
                'expect' => ['supplies' => ['web']],
            ]],

            'devices set' => [[
                'seed'   => ['devices' => ['desktop']],
                'post'   => ['targeting-devices' => ['phone', 'tablet']],
                'expect' => ['devices' => ['phone', 'tablet']],
            ]],
            'devices clear' => [[
                'seed'    => ['devices' => ['desktop']],
                'post'    => ['targeting-devices' => ['all']],
                'missing' => ['devices'],
            ]],
            'devices leave-alone' => [[
                'seed'   => ['devices' => ['desktop']],
                'post'   => [],
                'expect' => ['devices' => ['desktop']],
            ]],

            'schedule set' => [[
                'seed' => ['schedule' => $scheduleSeed],
                'post' => [
                    'targeting-schedule-on'     => '1',
                    'targeting-schedule-2-day'  => '1',
                    'targeting-schedule-2-from' => '08:00',
                    'targeting-schedule-2-to'   => '12:00',
                ],
                'expect' => ['schedule' => [[
                    'day'        => 'tuesday',
                    'start_time' => '08:00',
                    'end_time'   => '12:00',
                ]]],
            ]],
            'schedule clear' => [[
                'seed'    => ['schedule' => $scheduleSeed],
                'post'    => ['targeting-schedule-on' => '0'],
                'missing' => ['schedule'],
            ]],
            'schedule leave-alone' => [[
                'seed'   => ['schedule' => $scheduleSeed],
                'post'   => [],
                'expect' => ['schedule' => $scheduleSeed],
            ]],

            'gender set' => [[
                'seed'   => ['gender' => 1],
                'post'   => ['targeting-gender' => 'women'],
                'expect' => ['gender' => 2],
            ]],
            'gender clear' => [[
                'seed'    => ['gender' => 1],
                'post'    => ['targeting-gender' => 'all'],
                'missing' => ['gender'],
            ]],
            'gender leave-alone' => [[
                'seed'   => ['gender' => 1],
                'post'   => [],
                'expect' => ['gender' => 1],
            ]],

            'domain_lists set' => [[
                'seed'   => ['domain_lists' => ['exclude' => [5]]],
                'post'   => [
                    'targeting-domain_lists_mode' => 'include',
                    'targeting-domain_lists'      => ['7'],
                ],
                'expect' => ['domain_lists' => ['include' => [7]]],
            ]],
            'domain_lists clear' => [[
                'seed'    => ['domain_lists' => ['exclude' => [5]]],
                'post'    => ['targeting-domain_lists_mode' => 'exclude'],
                'missing' => ['domain_lists'],
            ]],
            'domain_lists leave-alone' => [[
                'seed'   => ['domain_lists' => ['exclude' => [5]]],
                'post'   => [],
                'expect' => ['domain_lists' => ['exclude' => [5]]],
            ]],

            'source_lists set' => [[
                'seed'   => ['source_lists' => [11]],
                'post'   => [
                    'source_lists_posted' => '1',
                    'source_lists'        => ['22'],
                ],
                'expect' => ['source_lists' => [22]],
            ]],
            'source_lists clear' => [[
                'seed'    => ['source_lists' => [11]],
                'post'    => ['source_lists_posted' => '1'],
                'missing' => ['source_lists'],
            ]],
            'source_lists leave-alone' => [[
                'seed'   => ['source_lists' => [11]],
                'post'   => [],
                'expect' => ['source_lists' => [11]],
            ]],

            'frequency set' => [[
                'seed'     => ['frequency' => 100],
                'freq_cap' => 100,
                'post'     => [
                    'targeting-frequency-mode' => 'set',
                    'targeting-frequency'      => '50',
                ],
                'expect'        => ['frequency' => 50],
                'expect_freq'   => 50,
            ]],
            'frequency clear' => [[
                'seed'     => ['frequency' => 100],
                'freq_cap' => 100,
                'post'     => [
                    'targeting-frequency-mode' => 'none',
                    'targeting-frequency'      => '1',
                ],
                'missing'     => ['frequency'],
                'expect_freq' => 0,
            ]],
            'frequency leave-alone' => [[
                'seed'        => ['frequency' => 100],
                'freq_cap'    => 100,
                'post'        => [],
                'expect'      => ['frequency' => 100],
                'expect_freq' => 100,
            ]],

            'camp_category_ids set' => [[
                'cats_seed'   => [901],
                'post'        => [
                    'camp_category_ids_posted' => '1',
                    'camp_category_ids'        => ['902'],
                ],
                'expect_cats' => [902],
            ]],
            'camp_category_ids clear' => [[
                'cats_seed'   => [901],
                'post'        => ['camp_category_ids_posted' => '1'],
                'expect_cats' => [],
            ]],
            'camp_category_ids leave-alone' => [[
                'cats_seed'   => [901],
                'post'        => [],
                'expect_cats' => [901],
            ]],

            'xml_traffic set' => [[
                'xml_seed'   => 0,
                'post'       => [
                    'source_lists_posted' => '1',
                    'xml_traffic'         => '1',
                ],
                'expect_xml' => 1,
            ]],
            'xml_traffic clear' => [[
                'xml_seed'   => 1,
                'post'       => [
                    'source_lists_posted' => '1',
                    'xml_traffic'         => '0',
                ],
                'expect_xml' => 0,
            ]],
            'xml_traffic leave-alone' => [[
                'xml_seed'   => 1,
                'post'       => [],
                'expect_xml' => 1,
            ]],
        ];
    }

    /**
     * @param array<string, mixed> $case
     */
    #[DataProvider('fieldCases')]
    public function testUpdateFieldMatrix(array $case): void
    {
        $id = $this->insertCampaign(
            $case['seed'] ?? [],
            (int) ($case['freq_cap'] ?? 0),
            (int) ($case['xml_seed'] ?? 0)
        );

        if (!empty($case['cats_seed'])) {
            $rows = [];
            foreach ($case['cats_seed'] as $catId) {
                $rows[] = [
                    'campaign_id' => $id,
                    'adv_id'      => self::ADV,
                    'cat_id'      => (int) $catId,
                ];
            }
            $this->conn->table('adv_camp_categories_mapping')->insertBatch($rows);
        }

        $this->req()->post('campaign/update/' . $id, $this->basePost() + $case['post']);

        $out = $this->reload($id);

        $this->assertSame(
            ['120438'],
            array_map('strval', (array) ($out['json']['affiliates'] ?? [])),
            'affiliates must survive every modeled-field save'
        );

        foreach ($case['expect'] ?? [] as $key => $value) {
            $this->assertSame($value, $out['json'][$key] ?? null, "json.$key");
        }
        foreach ($case['missing'] ?? [] as $key) {
            $this->assertArrayNotHasKey($key, $out['json'], "json.$key must be cleared");
        }
        if (array_key_exists('expect_freq', $case)) {
            $this->assertSame((int) $case['expect_freq'], (int) ($out['row']['freq_cap'] ?? -1));
        }
        if (array_key_exists('expect_xml', $case)) {
            $this->assertSame((int) $case['expect_xml'], (int) ($out['row']['xml_traffic'] ?? -1));
        }
        if (array_key_exists('expect_cats', $case)) {
            $cats = $this->conn->table('adv_camp_categories_mapping')
                ->select('cat_id')
                ->where('campaign_id', $id)
                ->where('adv_id', self::ADV)
                ->get()
                ->getResultArray();
            $ids = array_map(static fn ($r) => (int) $r['cat_id'], $cats);
            sort($ids);
            $expect = $case['expect_cats'];
            sort($expect);
            $this->assertSame($expect, $ids);
        }
    }

    public function testCreateDisplayPersistsConnectionTypesAndCategories(): void
    {
        $name = 'QA Create Conn Cat ' . uniqid('', true);
        $this->req()->post('campaigns/create/display', [
            'name'                       => $name,
            'bid'                        => '0.10',
            'budget'                     => '50',
            'status'                     => 'A',
            'campaign_type'              => 'Display',
            'location_posted'            => '1',
            'targeting-connection-types' => '0',
            'connection-type'            => ['wifi'],
            'targeting-categories_mode'  => 'include',
            'categories'                 => ['IAB1-1'],
        ]);

        $row = $this->conn->table('campaign')
            ->where('advertiser_id', self::ADV)
            ->where('name', $name)
            ->get()
            ->getRowArray();
        $this->assertNotNull($row, 'create must insert a campaign row');
        $this->campaignIds[] = (int) $row['id'];

        $targetingRow = $this->conn->table('campaign_targeting')
            ->where('campaign_id', $row['id'])->get()->getRowArray();
        $json = json_decode((string) ($targetingRow['json'] ?? '{}'), true) ?: [];

        $this->assertSame(['wifi'], $json['connection_types'] ?? null);
        $this->assertSame(['include' => ['IAB1-1']], $json['categories'] ?? null);
    }

    public function testCreateDisplayOmitsConnectionAndCategoriesWhenAllEmpty(): void
    {
        $name = 'QA Create Empty Targeting ' . uniqid('', true);
        $this->req()->post('campaigns/create/display', [
            'name'                       => $name,
            'bid'                        => '0.10',
            'budget'                     => '50',
            'status'                     => 'A',
            'campaign_type'              => 'Display',
            'location_posted'            => '1',
            'targeting-connection-types' => '1',
            'connection-type'            => ['wifi'],
            'targeting-categories_mode'  => 'include',
        ]);

        $row = $this->conn->table('campaign')
            ->where('advertiser_id', self::ADV)
            ->where('name', $name)
            ->get()
            ->getRowArray();
        $this->assertNotNull($row, 'create must insert a campaign row');
        $this->campaignIds[] = (int) $row['id'];

        $targetingRow = $this->conn->table('campaign_targeting')
            ->where('campaign_id', $row['id'])->get()->getRowArray();
        $json = json_decode((string) ($targetingRow['json'] ?? '{}'), true) ?: [];

        $this->assertArrayNotHasKey('connection_types', $json);
        $this->assertArrayNotHasKey('categories', $json);
        $this->assertSame(['USA'], $json['countries'] ?? null);
    }

    public function testUpdateSetsCampaignCreativesWhenPosted(): void
    {
        $id = $this->insertCampaign([]);
        $a = $this->insertCreative('QA Creative A');
        $b = $this->insertCreative('QA Creative B');
        $this->conn->table('campaign_creative_link')->insert([
            'campaign_id' => $id,
            'creative_id' => $a,
        ]);

        $this->req()->post('campaign/update/' . $id, $this->basePost() + [
            'creatives_posted' => '1',
            'creatives'        => [(string) $b],
        ]);

        $this->assertSame([$b], $this->linkedCreativeIds($id));
        $out = $this->reload($id);
        $this->assertSame(['120438'], array_map('strval', (array) ($out['json']['affiliates'] ?? [])));
    }

    public function testUpdateClearsCampaignCreativesWithSentinel(): void
    {
        $id = $this->insertCampaign([]);
        $a = $this->insertCreative('QA Creative Clear');
        $this->conn->table('campaign_creative_link')->insert([
            'campaign_id' => $id,
            'creative_id' => $a,
        ]);

        $this->req()->post('campaign/update/' . $id, $this->basePost() + [
            'creatives_posted' => '1',
        ]);

        $this->assertSame([], $this->linkedCreativeIds($id));
    }

    public function testUpdateLeavesCampaignCreativesWithoutSentinel(): void
    {
        $id = $this->insertCampaign([]);
        $a = $this->insertCreative('QA Creative Keep');
        $this->conn->table('campaign_creative_link')->insert([
            'campaign_id' => $id,
            'creative_id' => $a,
        ]);

        $this->req()->post('campaign/update/' . $id, $this->basePost());

        $this->assertSame([$a], $this->linkedCreativeIds($id));
    }

    public function testCreateDisplayLinksPostedCreatives(): void
    {
        $creativeId = $this->insertCreative('QA Create Link Creative');
        $name = 'QA Create Creatives ' . uniqid('', true);
        $this->req()->post('campaigns/create/display', [
            'name'              => $name,
            'bid'               => '0.10',
            'budget'            => '50',
            'status'            => 'A',
            'campaign_type'     => 'Display',
            'location_posted'   => '1',
            'creatives_posted'  => '1',
            'creatives'         => [(string) $creativeId],
        ]);

        $row = $this->conn->table('campaign')
            ->where('advertiser_id', self::ADV)
            ->where('name', $name)
            ->get()
            ->getRowArray();
        $this->assertNotNull($row, 'create must insert a campaign row');
        $this->campaignIds[] = (int) $row['id'];
        $this->assertSame([$creativeId], $this->linkedCreativeIds((int) $row['id']));
    }

    private function insertCreative(string $name): int
    {
        $this->conn->table('creatives')->insert([
            'aid'                => self::ADV,
            'creative_name'      => $name,
            'creative_type'      => 0,
            'status'             => 'A',
            'destination_url'    => 'https://example.com',
            'image_url'          => '',
            'img_width'          => 300,
            'img_height'         => 250,
            'creative_title'     => '',
            'creative_desc'      => '',
            'creative_url'       => 'example.com',
            'last_modified'      => date('Y-m-d H:i:s'),
            'parent_creative_id' => 0,
            'rank'               => 0,
        ]);

        return (int) $this->conn->insertID();
    }

    /**
     * @return list<int>
     */
    private function linkedCreativeIds(int $campaignId): array
    {
        $rows = $this->conn->table('campaign_creative_link')
            ->select('creative_id')
            ->where('campaign_id', $campaignId)
            ->get()
            ->getResultArray();
        $ids = array_map(static fn ($r) => (int) $r['creative_id'], $rows);
        sort($ids);

        return $ids;
    }
}
