<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\LaunchFlow;
use App\Models\Campaign;
use App\Models\User;
use App\Services\Meta\CreativeSpec;
use App\Services\Meta\GraphApi;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Who the ad appears as on Instagram.
 *
 * Instagram has been one of the offered placements since the start and nothing
 * ever attached an Instagram account, so those ads ran under the Facebook
 * Page's name and picture. Meta permits it, so nothing failed and nothing said
 * anything: the ads simply did not look like the brand.
 *
 * Two cases matter beyond the happy one. An advertiser with no Instagram
 * presence is ordinary and must still be able to finish a campaign, and an
 * account that was asked about and declined has to stay different from one
 * where the question never came up.
 */
class InstagramAccountTest extends TestCase
{
    use RefreshDatabase;

    protected Campaign $campaign;

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

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['act_777'],
            'platforms.meta.mcp.enabled' => false,
            'platforms.meta.token' => 'test-token',
        ]);

        $this->actingAs(User::factory()->create());

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-ig-0001');

        $this->campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
            'ad_account_id' => 'act_777',
            'page_id' => 'page-1',
            'placements' => ['facebook', 'instagram'],
        ]);

        $workspace->focusOn($this->campaign);
    }

    private function runTool(string $name, array $arguments = []): array
    {
        $tool = collect(app(ToolRegistry::class)->resolve())->first(fn ($t) => $t->name() === $name);

        $this->assertNotNull($tool, "Tool {$name} is not registered.");

        return json_decode((string) $tool->handle(new Request($arguments)), true);
    }

    private function fakeAccounts(): void
    {
        Http::fake([
            '*/page-1*' => Http::response([
                'connected_instagram_account' => ['id' => 'ig-page', 'username' => 'wintersale'],
            ]),
            '*/instagram_accounts*' => Http::response([
                'data' => [
                    ['id' => 'ig-page', 'username' => 'wintersale'],
                    ['id' => 'ig-other', 'username' => 'wintersale_uk'],
                ],
            ]),
            '*' => Http::response(['data' => []]),
        ]);
    }

    // -------------------------------------------------------------- the offer

    public function test_the_page_account_is_suggested_and_the_rest_are_still_shown(): void
    {
        $this->fakeAccounts();

        $this->runTool('campaign__choose_instagram_account');

        $offered = collect(app(Choices::class)->all())->firstWhere('field', 'instagram_account_id');

        $this->assertNotNull($offered, 'The accounts should be offered.');

        $byValue = collect($offered['options'])->keyBy('value');

        $this->assertSame('@wintersale', $byValue['ig-page']['label']);
        $this->assertSame('Suggested', $byValue['ig-page']['badge']);
        $this->assertSame('@wintersale_uk', $byValue['ig-other']['label']);
        $this->assertNull($byValue['ig-other']['badge'], 'Only the Page account is the suggestion.');

        // And a way to say no, or declining would mean typing.
        $this->assertSame('Use the Page instead', $byValue['']['label']);
    }

    /** The account is listed once even though it comes back from both edges. */
    public function test_the_same_account_on_both_edges_is_offered_once(): void
    {
        $this->fakeAccounts();

        $this->runTool('campaign__choose_instagram_account');

        $values = array_column(
            collect(app(Choices::class)->all())->firstWhere('field', 'instagram_account_id')['options'],
            'value',
        );

        $this->assertSame(['ig-page', 'ig-other', ''], $values);
    }

    public function test_choosing_one_attaches_it(): void
    {
        $this->fakeAccounts();

        $result = $this->runTool('campaign__choose_instagram_account', ['instagram_account_id' => 'ig-other']);

        $this->assertTrue($result['ok']);
        $this->assertSame('ig-other', $this->campaign->fresh()->instagram_account_id);
    }

    public function test_an_account_the_ad_account_cannot_use_is_refused(): void
    {
        $this->fakeAccounts();

        $result = $this->runTool('campaign__choose_instagram_account', ['instagram_account_id' => 'ig-invented']);

        $this->assertFalse($result['ok']);
        $this->assertNull($this->campaign->fresh()->instagram_account_id);
    }

    // ---------------------------------------------------- having none at all

    /**
     * An advertiser with no Instagram handle has to be able to finish.
     *
     * Recorded as answered rather than left null, or the flow asks again every
     * turn and the campaign can never be completed.
     */
    public function test_an_account_with_no_instagram_presence_is_recorded_as_answered(): void
    {
        Http::fake(['*' => Http::response(['data' => []])]);

        $result = $this->runTool('campaign__choose_instagram_account');

        $this->assertTrue($result['ok']);
        $this->assertSame([], $result['instagram_accounts']);
        $this->assertSame('', $this->campaign->fresh()->instagram_account_id);

        $this->assertTrue(collect(app(LaunchFlow::class)->progress($this->campaign->fresh()))
            ->firstWhere('key', 'instagram')['done']);
    }

    public function test_declining_is_recorded_as_an_answer(): void
    {
        $this->fakeAccounts();

        $this->runTool('campaign__choose_instagram_account', ['instagram_account_id' => '']);

        $this->assertSame('', $this->campaign->fresh()->instagram_account_id);
        $this->assertTrue(collect(app(LaunchFlow::class)->progress($this->campaign->fresh()))
            ->firstWhere('key', 'instagram')['done']);
    }

    // --------------------------------------------------------------- the flow

    /** A Facebook-only campaign is not missing an Instagram account. */
    public function test_a_campaign_with_no_instagram_placement_is_never_asked(): void
    {
        $this->campaign->update(['placements' => ['facebook']]);

        $keys = array_column(app(LaunchFlow::class)->progress($this->campaign->fresh()), 'key');

        $this->assertNotContains('instagram', $keys);
    }

    public function test_a_campaign_with_an_instagram_placement_is_asked(): void
    {
        $keys = array_column(app(LaunchFlow::class)->progress($this->campaign), 'key');

        $this->assertContains('instagram', $keys);
    }

    // ------------------------------------------------------------ what is sent

    /**
     * instagram_user_id sits beside object_story_spec, not inside it. Inside,
     * Meta ignores it and the ad still runs as the Page.
     */
    public function test_the_account_is_sent_alongside_the_story_spec(): void
    {
        Http::fake(['*' => Http::response(['id' => 'creative-1'])]);

        app(GraphApi::class)->createCreative(new CreativeSpec(
            adAccountId: 'act_777',
            pageId: 'page-1',
            imageHash: 'hash-1',
            landingUrl: 'https://example.com',
            headline: 'Winter Sale',
            primaryText: 'Half price.',
            callToAction: 'SHOP_NOW',
            name: 'creative 1',
            instagramUserId: 'ig-page',
        ));

        Http::assertSent(function ($request): bool {
            $data = $request->data();
            $spec = json_decode((string) ($data['object_story_spec'] ?? '{}'), true);

            return ($data['instagram_user_id'] ?? null) === 'ig-page'
                && ! isset($spec['instagram_user_id']);
        });
    }

    /** Meta reads a null as an explicit unset and rejects it, so it is omitted. */
    public function test_no_account_sends_no_field_at_all(): void
    {
        Http::fake(['*' => Http::response(['id' => 'creative-1'])]);

        app(GraphApi::class)->createCreative(new CreativeSpec(
            adAccountId: 'act_777',
            pageId: 'page-1',
            imageHash: 'hash-1',
            landingUrl: 'https://example.com',
            headline: 'Winter Sale',
            primaryText: 'Half price.',
            callToAction: 'SHOP_NOW',
            name: 'creative 1',
            instagramUserId: '',
        ));

        Http::assertSent(fn ($request): bool => ! array_key_exists('instagram_user_id', $request->data()));
    }

    /**
     * A Page linking Instagram the other way still offers the account.
     *
     * Meta keeps two Page fields for this and they can disagree: a Page can
     * carry instagram_business_account, which is the one the ads documentation
     * points at, without connected_instagram_account. Only the second was read,
     * so such a Page reported no Instagram presence at all and the buyer was
     * told the placements would run under the Page.
     */
    public function test_a_business_instagram_account_on_the_page_is_offered(): void
    {
        Http::fake([
            '*/page-1*' => Http::response([
                'instagram_business_account' => ['id' => 'ig-business', 'username' => 'wintersale'],
            ]),
            '*instagram_accounts*' => Http::response(['data' => []]),
            '*' => Http::response(['data' => []]),
        ]);

        $accounts = app(GraphApi::class)
            ->instagramAccounts('act_777', 'page-1');

        $this->assertCount(1, $accounts);
        $this->assertSame('ig-business', $accounts[0]['id']);
        $this->assertSame('wintersale', $accounts[0]['username']);
    }

    /** The same account under both fields is one account, not two. */
    public function test_the_two_page_fields_are_not_offered_twice(): void
    {
        Http::fake([
            '*/page-1*' => Http::response([
                'connected_instagram_account' => ['id' => 'ig-same', 'username' => 'wintersale'],
                'instagram_business_account' => ['id' => 'ig-same', 'username' => 'wintersale'],
            ]),
            '*instagram_accounts*' => Http::response(['data' => []]),
            '*' => Http::response(['data' => []]),
        ]);

        $this->assertCount(1, app(GraphApi::class)->instagramAccounts('act_777', 'page-1'));
    }
}
