<?php

namespace Tests\Feature;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;

/**
 * Real-user simulation for the Ad Ops Lists page (AP-30).
 *
 * Each test drives an actual HTTP request through the full stack — routing,
 * the auth + adops filters, the real Lists controllers, ListsService and
 * ListsRepository — against the dev Admin DB, then asserts the DB side effects
 * a user would expect. Everything runs under a throwaway advertiser id and is
 * cleaned up in tearDown so no real advertiser data is touched.
 *
 * Run with the feature bootstrap (defines the env/DB constants the web entry
 * normally sets) and newrelic disabled:
 *   php8.2 -d newrelic.enabled=0 -d newrelic.daemon.dont_launch=3 \
 *     vendor/bin/phpunit --bootstrap tests/feature_bootstrap.php tests/feature
 */
final class ListsFlowTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    /** Throwaway advertiser id — keeps the run isolated from real data. */
    private const ADV = 999999992;

    private array $sess;

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

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

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

        // The lists/* routes apply the CSRF filter to non-GET requests; drop it
        // for these in-process tests (we're exercising controller/DB behaviour,
        // not the token round-trip, which the browser/view already handles).
        $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
    {
        // keyword lists + their keyword rows
        $ids = $this->conn->table('keyword_lists')->select('id')->where('advertiser_id', self::ADV)->get()->getResultArray();
        foreach ($ids as $r) {
            $this->conn->table('rep_keywords_broad')->where('list_id', (int) $r['id'])->delete();
            $this->conn->table('rep_keywords_exact')->where('list_id', (int) $r['id'])->delete();
        }
        $this->conn->table('keyword_lists')->where('advertiser_id', self::ADV)->delete();

        // domain lists + domains
        $dids = $this->conn->table('domain_list_names')->select('list_id')->where('aid', self::ADV)->get()->getResultArray();
        foreach ($dids as $r) {
            $this->conn->table('ads_domain_list')->where('list_id', (int) $r['list_id'])->delete();
        }
        $this->conn->table('domain_list_names')->where('aid', self::ADV)->delete();

        // source lists + sources
        $sids = $this->conn->table('source_lists')->select('id')->where('advertiser_id', self::ADV)->get()->getResultArray();
        foreach ($sids as $r) {
            $this->conn->table('sources')->where('list_id', (int) $r['id'])->delete();
        }
        $this->conn->table('source_lists')->where('advertiser_id', self::ADV)->delete();
    }

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

    /* ───────────────── keyword lists ───────────────── */

    public function testUserCreatesKeywordListWithBid(): void
    {
        $this->req()->post('lists/keyword-lists/create', [
            'name'     => 'QA KW List',
            'type'     => 'broad',
            'bid'      => '0.250000',
            'keywords' => "red shoes\nblue shoes",
        ]);

        $list = $this->conn->table('keyword_lists')
            ->where('advertiser_id', self::ADV)->where('name', 'QA KW List')
            ->get()->getRowArray();

        $this->assertNotNull($list, 'keyword list row should exist');
        $this->assertSame('broad', $list['type']);
        $this->assertSame('0.250000', (string) $list['bid']);

        $count = $this->conn->table('rep_keywords_broad')
            ->where('list_id', (int) $list['id'])->where('status', 1)->countAllResults();
        $this->assertSame(2, $count, 'both keywords stored');
    }

    public function testUserSearchesFindReplacesAndDeletesKeywords(): void
    {
        // Arrange: a list with two keywords.
        $this->req()->post('lists/keyword-lists/create', [
            'name'     => 'QA FR List',
            'type'     => 'broad',
            'keywords' => "alpha widget\nbeta widget",
        ]);
        $listId = (int) $this->conn->table('keyword_lists')
            ->where('advertiser_id', self::ADV)->where('name', 'QA FR List')
            ->get()->getRowArray()['id'];

        // Search finds them.
        $search = $this->req()->get('lists/keywords?q=widget&offset=0&limit=50');
        $search->assertStatus(200);
        $items = json_decode($search->getJSON(), true)['items'];
        $kw    = array_column($items, 'keyword');
        $this->assertContains('alpha widget', $kw);
        $this->assertContains('beta widget', $kw);

        // Find & replace widget -> gadget on this list.
        $this->req()->post('lists/keywords/find-replace', [
            'find'     => 'widget',
            'replace'  => 'gadget',
            'list_ids' => [$listId],
        ]);
        $after = $this->conn->table('rep_keywords_broad')
            ->where('list_id', $listId)->where('status', 1)
            ->orderBy('keyword')->get()->getResultArray();
        $this->assertSame(['alpha gadget', 'beta gadget'], array_column($after, 'keyword'));

        // Remove Selected: soft-delete the first one by composite id.
        $firstId = (int) $after[0]['id'];
        $this->req()->post('lists/keywords/delete', ['ids' => [$firstId . '_broad']]);
        $row = $this->conn->table('rep_keywords_broad')->where('id', $firstId)->get()->getRowArray();
        $this->assertSame(0, (int) $row['status'], 'selected keyword soft-deleted');
    }

    public function testInvalidKeywordTypeDoesNotBreakCreate(): void
    {
        // The form only offers broad/exact now; a stray value normalizes to broad
        // instead of the old 422 that produced "Error while saving".
        $this->req()->post('lists/keyword-lists/create', [
            'name'     => 'QA Norm List',
            'type'     => 'phrase', // not a valid enum
            'keywords' => 'normalize me',
        ]);
        $list = $this->conn->table('keyword_lists')
            ->where('advertiser_id', self::ADV)->where('name', 'QA Norm List')
            ->get()->getRowArray();
        $this->assertNotNull($list);
        $this->assertSame('broad', $list['type']);
    }

    /* ───────────────── domains ───────────────── */

    public function testUserCreatesDomainList(): void
    {
        $this->req()->post('lists/domains/create', [
            'name'    => 'QA Domains',
            'domains' => "https://Example.com/path\nfoo.bar.io\nnot a domain",
        ]);
        $list = $this->conn->table('domain_list_names')
            ->where('aid', self::ADV)->where('list_name', 'QA Domains')
            ->get()->getRowArray();
        $this->assertNotNull($list);

        $domains = array_column(
            $this->conn->table('ads_domain_list')->select('domain')->where('list_id', (int) $list['list_id'])->get()->getResultArray(),
            'domain'
        );
        $this->assertContains('example.com', $domains);
        $this->assertContains('foo.bar.io', $domains);
        $this->assertNotContains('not a domain', $domains, 'invalid entries are dropped');
    }

    /* ───────────────── sources ───────────────── */

    public function testUserCreatesSourceListWithStructuredRows(): void
    {
        $this->req()->post('lists/sources/create', [
            'name'    => 'QA Sources',
            'sources' => [
                ['source_id' => '111', 'bid' => '0.25'],
                ['source_id' => '222', 'bid' => '0'],
            ],
        ]);
        $list = $this->conn->table('source_lists')
            ->where('advertiser_id', self::ADV)->where('name', 'QA Sources')
            ->get()->getRowArray();
        $this->assertNotNull($list);

        $rows = $this->conn->table('sources')->where('list_id', (int) $list['id'])->orderBy('source_id')->get()->getResultArray();
        $this->assertCount(2, $rows);
        $this->assertSame(111, (int) $rows[0]['source_id']);
        $this->assertSame('0.250000', (string) $rows[0]['bid']);
        $this->assertSame(222, (int) $rows[1]['source_id']);
        $this->assertSame('0.000000', (string) $rows[1]['bid'], 'bid 0 (block) is allowed');
    }

    /* ───────────────── auth ───────────────── */

    public function testUnauthenticatedIsBlocked(): void
    {
        $result = $this->get('lists/keywords?q=x');
        $this->assertContains($result->response()->getStatusCode(), [301, 302, 401]);
    }
}
