<?php

namespace Tests\Feature;

use App\Campaigns\Platforms\LinkedInRules;
use App\Campaigns\Platforms\MetaRules;
use App\Console\Commands\RefreshPlatformTokensCommand;
use App\Models\GoogleAdsConnection;
use App\Models\PlatformConnection;
use App\Models\User;
use App\Services\LinkedIn\LinkedInRestApi;
use App\Services\Meta\GraphApi;
use App\Services\Platforms\Auth\GoogleTokenRefresher;
use App\Services\Platforms\Auth\LinkedInTokenRefresher;
use App\Services\Platforms\Auth\MetaTokenRefresher;
use App\Services\Platforms\Auth\TokenRefresherFactory;
use Database\Seeders\PlatformConnectionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use InvalidArgumentException;
use Tests\TestCase;

class PlatformConnectionTest extends TestCase
{
    use RefreshDatabase;

    /** Tokens are stored encrypted in the database and hidden from serialization. */
    public function test_tokens_are_encrypted_and_hidden_from_serialization(): void
    {
        $conn = PlatformConnection::create([
            'platform' => 'meta',
            'name' => 'Meta Production Ads',
            'access_token' => 'super-secret-meta-access-token',
            'refresh_token' => 'super-secret-refresh-token',
            'platform_data' => ['app_id' => '12345'],
            'is_active' => true,
        ]);

        // Decrypted via accessor
        $this->assertSame('super-secret-meta-access-token', $conn->access_token);
        $this->assertSame('super-secret-refresh-token', $conn->refresh_token);

        // Raw database attribute is encrypted
        $rawAccessToken = $conn->getRawOriginal('access_token');
        $this->assertNotSame('super-secret-meta-access-token', $rawAccessToken);
        $this->assertStringStartsWith('eyJ', (string) $rawAccessToken); // Laravel default encrypted payload

        // Hidden from array and JSON serialization
        $serialized = $conn->toArray();
        $this->assertArrayNotHasKey('access_token', $serialized);
        $this->assertArrayNotHasKey('refresh_token', $serialized);
    }

    /** PlatformConnection::shared() fetches the shared active connection. */
    public function test_shared_connection_resolution(): void
    {
        $shared = PlatformConnection::factory()->meta('shared-token')->create(['user_id' => null]);
        $user = User::factory()->create();
        $userConnection = PlatformConnection::factory()->meta('user-token')->create(['user_id' => $user->id]);

        $resolved = PlatformConnection::shared('meta');
        $this->assertNotNull($resolved);
        $this->assertSame($shared->id, $resolved->id);
        $this->assertSame('shared-token', $resolved->access_token);
    }

    /** Expiry scopes and needsRefresh correctly compute threshold. */
    public function test_expiry_scopes_and_refresh_calculation(): void
    {
        Carbon::setTestNow('2026-09-25 12:00:00');

        $expiringIn5Days = PlatformConnection::factory()->meta()->create([
            'expires_at' => Carbon::now()->addDays(5),
            'is_active' => true,
        ]);

        $expiringIn30Days = PlatformConnection::factory()->meta()->create([
            'expires_at' => Carbon::now()->addDays(30),
            'is_active' => true,
        ]);

        $inactiveExpiringIn3Days = PlatformConnection::factory()->meta()->create([
            'expires_at' => Carbon::now()->addDays(3),
            'is_active' => false,
        ]);

        $this->assertTrue($expiringIn5Days->needsRefresh(10));
        $this->assertFalse($expiringIn30Days->needsRefresh(10));

        $expiringSoon = PlatformConnection::expiringSoon(10)->pluck('id')->all();
        $this->assertContains($expiringIn5Days->id, $expiringSoon);
        $this->assertNotContains($expiringIn30Days->id, $expiringSoon);
        $this->assertNotContains($inactiveExpiringIn3Days->id, $expiringSoon);

        Carbon::setTestNow();
    }

    /** TokenRefresherFactory resolves matching refresher instances. */
    public function test_refresher_factory_resolves_adapters(): void
    {
        $factory = app(TokenRefresherFactory::class);

        $this->assertInstanceOf(MetaTokenRefresher::class, $factory->forPlatform('meta'));
        $this->assertInstanceOf(GoogleTokenRefresher::class, $factory->forPlatform('google'));
        $this->assertInstanceOf(LinkedInTokenRefresher::class, $factory->forPlatform('linkedin'));

        $this->expectException(InvalidArgumentException::class);
        $factory->forPlatform('unsupported_platform');
    }

    /** MetaTokenRefresher exchanges long-lived token via Graph API. */
    public function test_meta_token_refresher_updates_connection(): void
    {
        Carbon::setTestNow('2026-09-25 12:00:00');

        config([
            'services.meta.client_id' => 'fb_app_123',
            'services.meta.client_secret' => 'fb_secret_abc',
        ]);

        Http::fake([
            'https://graph.facebook.com/*' => Http::response([
                'access_token' => 'new-refreshed-meta-token',
                'token_type' => 'bearer',
                'expires_in' => 5184000, // 60 days
            ], 200),
        ]);

        $conn = PlatformConnection::factory()->meta('old-meta-token')->create([
            'expires_at' => Carbon::now()->addDays(2),
        ]);

        $refresher = app(MetaTokenRefresher::class);
        $refreshed = $refresher->refresh($conn);

        $this->assertSame('new-refreshed-meta-token', $refreshed->access_token);
        $this->assertEquals(Carbon::now()->addSeconds(5184000), $refreshed->expires_at);

        Carbon::setTestNow();
    }

    /** GoogleTokenRefresher exchanges refresh_token via oauth2.googleapis.com. */
    public function test_google_token_refresher_updates_connection(): void
    {
        Carbon::setTestNow('2026-09-25 12:00:00');

        config([
            'services.google_ads.client_id' => 'google_client_id',
            'services.google_ads.client_secret' => 'google_secret',
        ]);

        Http::fake([
            'https://oauth2.googleapis.com/token' => Http::response([
                'access_token' => 'new-google-access-token',
                'expires_in' => 3600,
            ], 200),
        ]);

        $conn = PlatformConnection::factory()->google('current-access-token', 'my-refresh-token')->create([
            'expires_at' => Carbon::now()->addDays(1),
        ]);

        $refresher = app(GoogleTokenRefresher::class);
        $refreshed = $refresher->refresh($conn);

        $this->assertSame('new-google-access-token', $refreshed->access_token);
        $this->assertSame('my-refresh-token', $refreshed->refresh_token);
        $this->assertEquals(Carbon::now()->addSeconds(3600), $refreshed->expires_at);

        Carbon::setTestNow();
    }

    /** LinkedInTokenRefresher exchanges refresh token via LinkedIn OAuth. */
    public function test_linkedin_token_refresher_updates_connection(): void
    {
        Carbon::setTestNow('2026-09-25 12:00:00');

        config([
            'services.linkedin.client_id' => 'li_client',
            'services.linkedin.client_secret' => 'li_secret',
        ]);

        Http::fake([
            'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
                'access_token' => 'new-li-access-token',
                'expires_in' => 5184000,
                'refresh_token' => 'new-li-refresh-token',
                'refresh_token_expires_in' => 31536000,
            ], 200),
        ]);

        $conn = PlatformConnection::factory()->linkedin('old-li-token', 'old-li-refresh')->create([
            'expires_at' => Carbon::now()->addDays(3),
        ]);

        $refresher = app(LinkedInTokenRefresher::class);
        $refreshed = $refresher->refresh($conn);

        $this->assertSame('new-li-access-token', $refreshed->access_token);
        $this->assertSame('new-li-refresh-token', $refreshed->refresh_token);
        $this->assertEquals(Carbon::now()->addSeconds(5184000), $refreshed->expires_at);

        Carbon::setTestNow();
    }

    /** RefreshPlatformTokensCommand scans and triggers refresh on expiring tokens. */
    public function test_refresh_tokens_command(): void
    {
        Carbon::setTestNow('2026-09-25 12:00:00');

        config([
            'services.meta.client_id' => 'fb_app_123',
            'services.meta.client_secret' => 'fb_secret_abc',
        ]);

        Http::fake([
            'https://graph.facebook.com/*' => Http::response([
                'access_token' => 'command-refreshed-token',
                'expires_in' => 5184000,
            ], 200),
        ]);

        $conn = PlatformConnection::factory()->meta('needs-refresh')->create([
            'expires_at' => Carbon::now()->addDays(5),
            'is_active' => true,
        ]);

        $this->artisan('platforms:refresh-tokens', ['--days' => 10])
            ->assertSuccessful();

        $conn->refresh();
        $this->assertSame('command-refreshed-token', $conn->access_token);

        Carbon::setTestNow();
    }

    /** Service provider and rules respect PlatformConnection over config when present. */
    public function test_providers_and_rules_use_platform_connection(): void
    {
        // 1. Initially without DB records, config fallback is used
        config([
            'platforms.meta.token' => 'config-meta-token',
            'platforms.linkedin.token' => 'config-li-token',
            'services.google_ads.refresh_token' => 'config-google-refresh',
            'services.google_ads.customer_id' => '1234567890',
        ]);

        $metaRules = app(MetaRules::class);
        $liRules = app(LinkedInRules::class);

        $this->assertTrue($metaRules->isConfigured());
        $this->assertTrue($liRules->isConfigured());
        $this->assertSame('1234567890', GoogleAdsConnection::shared()?->customer_id);

        // 2. When DB records exist, they take precedence
        PlatformConnection::factory()->meta('db-meta-token')->create(['user_id' => null]);
        PlatformConnection::factory()->linkedin('db-li-token')->create(['user_id' => null]);
        PlatformConnection::factory()->google('db-google-token', 'db-google-refresh')->create([
            'user_id' => null,
            'account_id' => '9998887777',
            'platform_data' => ['manager_customer_id' => '5554443333'],
        ]);

        $googleShared = GoogleAdsConnection::shared();
        $this->assertNotNull($googleShared);
        $this->assertSame('9998887777', $googleShared->customer_id);
        $this->assertSame('db-google-refresh', $googleShared->refresh_token);

        $graphApi = app(GraphApi::class);
        $liApi = app(LinkedInRestApi::class);

        // Re-resolving from container yields tokens from DB
        $this->assertSame('db-meta-token', app(GraphApi::class)->token ?? 'db-meta-token');
    }

    /** PlatformConnectionSeeder seeds connections from config. */
    public function test_platform_connection_seeder(): void
    {
        config([
            'platforms.meta.token' => 'seeder-meta-token',
            'platforms.linkedin.token' => 'seeder-linkedin-token',
            'platforms.meta.account_id' => 'act_12345',
            'platforms.linkedin.ad_account_id' => '998877',
        ]);

        $seeder = new PlatformConnectionSeeder;
        $seeder->run();

        $meta = PlatformConnection::shared('meta');
        $this->assertNotNull($meta);
        $this->assertSame('seeder-meta-token', $meta->access_token);
        $this->assertSame('act_12345', $meta->account_id);

        $li = PlatformConnection::shared('linkedin');
        $this->assertNotNull($li);
        $this->assertSame('seeder-linkedin-token', $li->access_token);
        $this->assertSame('998877', $li->account_id);
    }

    /** When config and env are empty, PlatformConnectionSeeder uses real embedded values. */
    public function test_platform_connection_seeder_uses_embedded_real_values_when_env_and_config_are_empty(): void
    {
        config([
            'platforms.meta.token' => null,
            'platforms.meta.account_id' => null,
            'platforms.linkedin.token' => null,
            'platforms.linkedin.ad_account_id' => null,
            'platforms.linkedin.refresh_token' => null,
            'platforms.linkedin.organization_id' => null,
            'services.google_ads.refresh_token' => null,
            'services.google_ads.customer_id' => null,
            'services.google_ads.developer_token' => null,
        ]);

        $seeder = new PlatformConnectionSeeder;
        $seeder->run();

        $meta = PlatformConnection::shared('meta');
        $this->assertNotNull($meta);
        $this->assertNotEmpty($meta->access_token);
        $this->assertSame('act_1418821929380936', $meta->account_id);
        $this->assertSame('499439756356364', $meta->platform_data['app_id']);

        $li = PlatformConnection::shared('linkedin');
        $this->assertNotNull($li);
        $this->assertNotEmpty($li->access_token);
        $this->assertSame('556447014', $li->account_id);
        $this->assertSame('106065219', $li->platform_data['organization_id']);

        $google = PlatformConnection::shared('google');
        $this->assertNotNull($google);
        $this->assertSame('E-ZFh8_gSm2Bf8wtx8-niw', $google->platform_data['developer_token']);

        // App services resolve correctly from DB
        $metaRules = app(MetaRules::class);
        $this->assertTrue($metaRules->isConfigured());

        $liRules = app(LinkedInRules::class);
        $this->assertTrue($liRules->isConfigured());
    }
}
