<?php

namespace Tests\Unit\AdOps;

use App\Services\AdOps\EventLogService;
use App\Services\AdOps\ListsRepository;
use App\Services\AdOps\ListsService;
use PHPUnit\Framework\TestCase;

/**
 * ListsService now talks to the Admin DB through ListsRepository (no HTTP hop).
 * These tests inject a fake repository to lock in the orchestration contract:
 * argument translation (esp. keyword bid semantics), return-shape wrapping that
 * the Lists controllers depend on, and the empty-delete guard. DB behaviour
 * itself lives in ListsRepository and is exercised against the dev DB.
 */
final class ListsServiceTest extends TestCase
{
    private function make(): array
    {
        $repo = new FakeListsRepository();
        $svc  = new ListsService($repo, new NullEventLog());
        return [$svc, $repo];
    }

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

    public function testCreateKeywordListDelegatesAndWrapsId(): void
    {
        [$svc, $repo] = $this->make();
        $repo->createKeywordListReturn = 9;

        $res = $svc->createKeywordList('My List', ['kw'], 'exact', '0.50');

        $this->assertSame(['My List', ['kw'], 'exact', '0.50'], $repo->calls['createKeywordList']);
        $this->assertTrue($res['status']);
        $this->assertSame(9, $res['body']['id']);
    }

    public function testCreateKeywordListDefaultsTypeToBroad(): void
    {
        [$svc, $repo] = $this->make();
        $svc->createKeywordList('L', ['k']);
        $this->assertSame('broad', $repo->calls['createKeywordList'][2]);
    }

    public function testCreateKeywordListStatusFalseWhenRepoReturnsNull(): void
    {
        [$svc, $repo] = $this->make();
        $repo->createKeywordListReturn = null;
        $this->assertFalse($svc->createKeywordList('L', ['k'])['status']);
    }

    public function testUpdateKeywordListLeavesBidUntouchedByDefault(): void
    {
        [$svc, $repo] = $this->make();
        // No bid arg → $bid defaults to false → setBid must be false.
        $svc->updateKeywordList(7, 'N', ['k'], 'broad');

        [$id, $name, $keywords, $type, $setBid, $bid] = $repo->calls['updateKeywordList'];
        $this->assertSame(7, $id);
        $this->assertSame(['k'], $keywords);
        $this->assertSame('broad', $type);
        $this->assertFalse($setBid, 'No bid arg must not touch the stored bid');
        $this->assertNull($bid);
    }

    public function testUpdateKeywordListEmptyBidClears(): void
    {
        [$svc, $repo] = $this->make();
        $svc->updateKeywordList(7, 'N', null, null, '');

        [, , $keywords, $type, $setBid, $bid] = $repo->calls['updateKeywordList'];
        $this->assertNull($keywords, 'null keywords = keep current');
        $this->assertNull($type, 'null type = keep current');
        $this->assertTrue($setBid);
        $this->assertSame('', $bid);
    }

    public function testUpdateKeywordListSetsExplicitBid(): void
    {
        [$svc, $repo] = $this->make();
        $svc->updateKeywordList(7, 'N', null, null, '0.25');
        [, , , , $setBid, $bid] = $repo->calls['updateKeywordList'];
        $this->assertTrue($setBid);
        $this->assertSame('0.25', $bid);
    }

    public function testDeleteKeywordListReturnsRepoBool(): void
    {
        [$svc, $repo] = $this->make();
        $repo->deleteKeywordListReturn = true;
        $this->assertTrue($svc->deleteKeywordList(3));
        $this->assertSame(3, $repo->calls['deleteKeywordList'][0]);
    }

    // ───────────────── flat keywords

    public function testSearchKeywordsDelegates(): void
    {
        [$svc, $repo] = $this->make();
        $repo->searchKeywordsReturn = ['items' => [['id' => '5_broad']], 'total' => 1];

        $res = $svc->searchKeywords('shoe', 20, 50);

        $this->assertSame(['shoe', 20, 50], $repo->calls['searchKeywords']);
        $this->assertSame(1, $res['total']);
        $this->assertSame('5_broad', $res['items'][0]['id']);
    }

    public function testDeleteKeywordsRejectsEmptyWithoutCalling(): void
    {
        [$svc, $repo] = $this->make();
        $this->assertFalse($svc->deleteKeywords([]));
        $this->assertArrayNotHasKey('deleteKeywords', $repo->calls);
    }

    public function testDeleteKeywordsDelegatesCompositeIds(): void
    {
        [$svc, $repo] = $this->make();
        $this->assertTrue($svc->deleteKeywords(['5_broad', '9_exact']));
        $this->assertSame(['5_broad', '9_exact'], $repo->calls['deleteKeywords'][0]);
    }

    public function testFindAndReplaceWrapsAffectedCount(): void
    {
        [$svc, $repo] = $this->make();
        $repo->findReplaceKeywordsReturn = 3;

        $res = $svc->findAndReplaceKeywords('old', 'new', [3, 4]);

        $this->assertSame(['old', 'new', [3, 4]], $repo->calls['findReplaceKeywords']);
        $this->assertTrue($res['status']);
        $this->assertSame(3, $res['body']['affected']);
    }

    // ───────────────── domains

    public function testCreateDomainListWrapsId(): void
    {
        [$svc, $repo] = $this->make();
        $repo->createDomainListReturn = 5;

        $res = $svc->createDomainList('D', ['a.com', 'b.com']);

        $this->assertSame(['D', ['a.com', 'b.com']], $repo->calls['createDomainList']);
        $this->assertTrue($res['status']);
        $this->assertSame(5, $res['body']['id']);
    }

    public function testUpdateDomainListNullKeepsDomains(): void
    {
        [$svc, $repo] = $this->make();
        $svc->updateDomainList(2, 'D', null);
        $this->assertNull($repo->calls['updateDomainList'][2]);
    }

    public function testUpdateDomainListArrayReplaces(): void
    {
        [$svc, $repo] = $this->make();
        $svc->updateDomainList(2, 'D', ['x.com']);
        $this->assertSame(['x.com'], $repo->calls['updateDomainList'][2]);
    }

    // ───────────────── sources

    public function testCreateSourceListWrapsId(): void
    {
        [$svc, $repo] = $this->make();
        $repo->createSourceListReturn = 11;
        $sources = [['source_id' => 1, 'bid' => 0.25]];

        $res = $svc->createSourceList('Src', $sources);

        $this->assertSame(['Src', $sources], $repo->calls['createSourceList']);
        $this->assertTrue($res['status']);
        $this->assertSame(11, $res['body']['id']);
    }

    public function testUpdateSourceListArrayPassedThrough(): void
    {
        [$svc, $repo] = $this->make();
        $rows = [['source_id' => 2, 'bid' => 0.3]];
        $svc->updateSourceList(4, 'S', $rows);
        $this->assertSame($rows, $repo->calls['updateSourceList'][2]);
    }

    public function testGetSourcesForListDelegates(): void
    {
        [$svc, $repo] = $this->make();
        $rows = [['source_id' => 1, 'bid' => '0.100000']];
        $repo->getSourcesReturn = $rows;
        $this->assertSame($rows, $svc->getSourcesForList(5));
        $this->assertSame(5, $repo->calls['getSources'][0]);
    }

    // ───────────────── list getters

    public function testListGettersDelegateToRepo(): void
    {
        [$svc, $repo] = $this->make();
        $repo->domainListsReturn  = [['id' => 1, 'name' => 'D']];
        $repo->keywordListsReturn = [['id' => 2, 'name' => 'K', 'type' => 'broad', 'bid' => null]];
        $repo->sourceListsReturn  = [['id' => 3, 'name' => 'S']];

        $this->assertSame($repo->domainListsReturn, $svc->listDomainLists());
        $this->assertSame($repo->keywordListsReturn, $svc->listKeywordLists());
        $this->assertSame($repo->sourceListsReturn, $svc->listSources());
    }
}

/**
 * Records the args each method was called with and returns configurable canned
 * values. Skips the real constructor so no session / DB connection is touched.
 */
class FakeListsRepository extends ListsRepository
{
    public array $calls = [];

    public $createKeywordListReturn = 1;
    public $deleteKeywordListReturn = true;
    public $searchKeywordsReturn = ['items' => [], 'total' => 0];
    public $findReplaceKeywordsReturn = 0;
    public $createDomainListReturn = 1;
    public $createSourceListReturn = 1;
    public $getSourcesReturn = [];
    public $domainListsReturn = [];
    public $keywordListsReturn = [];
    public $sourceListsReturn = [];

    public function __construct()
    {
        // Intentionally does not call parent::__construct().
    }

    // domains
    public function domainLists(): array { $this->calls['domainLists'] = []; return $this->domainListsReturn; }
    public function getDomainList(int $id): ?array { $this->calls['getDomainList'] = [$id]; return ['id' => $id, 'name' => 'D']; }
    public function getDomains(int $id, int $offset = 0, int $limit = 5000): array { $this->calls['getDomains'] = [$id, $offset, $limit]; return []; }
    public function createDomainList(string $name, array $domains): ?int { $this->calls['createDomainList'] = [$name, $domains]; return $this->createDomainListReturn; }
    public function updateDomainList(int $id, string $name, ?array $domains): bool { $this->calls['updateDomainList'] = [$id, $name, $domains]; return true; }
    public function deleteDomainList(int $id): bool { $this->calls['deleteDomainList'] = [$id]; return true; }

    // keyword lists
    public function keywordLists(): array { $this->calls['keywordLists'] = []; return $this->keywordListsReturn; }
    public function getKeywordList(int $id): ?array { $this->calls['getKeywordList'] = [$id]; return ['id' => $id, 'name' => 'K', 'type' => 'broad', 'bid' => null]; }
    public function createKeywordList(string $name, array $keywords, string $type, $bid = null): ?int { $this->calls['createKeywordList'] = [$name, $keywords, $type, $bid]; return $this->createKeywordListReturn; }
    public function updateKeywordList(int $id, string $name, ?array $keywords, ?string $type, bool $setBid, $bid = null): bool { $this->calls['updateKeywordList'] = [$id, $name, $keywords, $type, $setBid, $bid]; return true; }
    public function deleteKeywordList(int $id): bool { $this->calls['deleteKeywordList'] = [$id]; return $this->deleteKeywordListReturn; }
    public function searchKeywords(string $search, int $offset, int $limit): array { $this->calls['searchKeywords'] = [$search, $offset, $limit]; return $this->searchKeywordsReturn; }
    public function deleteKeywords(array $compositeIds): int { $this->calls['deleteKeywords'] = [$compositeIds]; return count($compositeIds); }
    public function findReplaceKeywords(string $find, string $replace, array $listIds = []): int { $this->calls['findReplaceKeywords'] = [$find, $replace, $listIds]; return $this->findReplaceKeywordsReturn; }

    // sources
    public function sourceLists(): array { $this->calls['sourceLists'] = []; return $this->sourceListsReturn; }
    public function getSourceList(int $id): ?array { $this->calls['getSourceList'] = [$id]; return ['id' => $id, 'name' => 'S']; }
    public function getSources(int $id): array { $this->calls['getSources'] = [$id]; return $this->getSourcesReturn; }
    public function createSourceList(string $name, array $sources): ?int { $this->calls['createSourceList'] = [$name, $sources]; return $this->createSourceListReturn; }
    public function updateSourceList(int $id, string $name, ?array $sources): bool { $this->calls['updateSourceList'] = [$id, $name, $sources]; return true; }
    public function deleteSourceList(int $id): bool { $this->calls['deleteSourceList'] = [$id]; return true; }
}

/** Audit logger stub — swallows writes so tests never touch the DB. */
class NullEventLog extends EventLogService
{
    public function __construct()
    {
        // Intentionally does not call parent::__construct().
    }

    public function add(array $data)
    {
        return true;
    }
}
