<?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\Keywords\GoogleAdsClientFactory;
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\Contracts\Encryption\DecryptException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
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'],
            'platform_secrets' => ['app_secret' => 'private-test-secret'],
            '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
        $this->assertSame('{"app_id":"12345"}', $conn->getRawOriginal('platform_data'));
        $this->assertNotSame('{"app_secret":"private-test-secret"}', $conn->getRawOriginal('platform_secrets'));
        $this->assertSame('private-test-secret', $conn->platform_secrets['app_secret']);

        // Hidden from array and JSON serialization
        $serialized = $conn->toArray();
        $this->assertArrayNotHasKey('access_token', $serialized);
        $this->assertArrayNotHasKey('refresh_token', $serialized);
        $this->assertArrayNotHasKey('platform_secrets', $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),
            'platform_secrets' => ['app_secret' => 'db-meta-secret'],
        ]);

        $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);
        Http::assertSent(fn ($request): bool => $request['client_secret'] === 'db-meta-secret');

        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),
            'platform_secrets' => ['client_secret' => 'db-google-secret'],
        ]);

        $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);
        Http::assertSent(fn ($request): bool => $request['client_secret'] === 'db-google-secret');

        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),
            'platform_secrets' => ['client_secret' => 'db-linkedin-secret'],
        ]);

        $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);
        Http::assertSent(fn ($request): bool => $request['client_secret'] === 'db-linkedin-secret');

        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',
            'platforms.linkedin.client_secret' => 'seeded-client-secret',
        ]);

        $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);
        $this->assertSame('seeded-client-secret', $li->platform_secrets['client_secret']);
        $this->assertStringNotContainsString('seeded-client-secret', (string) $li->getRawOriginal('platform_secrets'));
    }

    public function test_platform_connection_seeder_keeps_fallbacks_for_all_three_platforms(): 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' => 'test-google-refresh-token',
            'services.google_ads.customer_id' => null,
            'services.google_ads.developer_token' => null,
        ]);

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

        $this->assertSame(3, PlatformConnection::count());

        $meta = PlatformConnection::shared('meta');
        $this->assertNotNull($meta);
        $this->assertNotEmpty($meta->access_token);
        $this->assertNotEmpty($meta->account_id);
        $this->assertNotEmpty($meta->platform_data['app_id']);
        $this->assertNotEmpty($meta->platform_secrets['app_secret']);

        $linkedin = PlatformConnection::shared('linkedin');
        $this->assertNotNull($linkedin);
        $this->assertNotEmpty($linkedin->access_token);
        $this->assertNotEmpty($linkedin->refresh_token);
        $this->assertNotEmpty($linkedin->platform_data['organization_id']);
        $this->assertNotEmpty($linkedin->platform_secrets['client_secret']);

        $google = PlatformConnection::shared('google');
        $this->assertNotNull($google);
        $this->assertSame('test-google-refresh-token', $google->refresh_token);
        $this->assertNotEmpty($google->platform_data['client_id']);
        $this->assertNotEmpty($google->platform_secrets['developer_token']);
        $this->assertNotEmpty($google->platform_secrets['client_secret']);
    }

    public function test_platform_connection_seeder_preserves_existing_tokens(): void
    {
        $existing = PlatformConnection::factory()->meta('keep-this-token')->create([
            'user_id' => null,
            'account_id' => null,
            'platform_data' => ['existing_id' => 'keep-this-id'],
            'platform_secrets' => ['mcp_token' => 'keep-this-secret'],
        ]);
        config([
            'platforms.meta.token' => 'new-seeder-token',
            'platforms.meta.account_id' => 'act_from_config',
            'services.meta.client_id' => 'config-app-id',
            'services.meta.client_secret' => 'config-app-secret',
            'platforms.meta.mcp.token' => 'new-mcp-token',
        ]);
        (new PlatformConnectionSeeder)->run();

        $existing->refresh();
        $this->assertSame('keep-this-token', $existing->access_token);
        $this->assertSame('act_from_config', $existing->account_id);
        $this->assertSame('keep-this-id', $existing->platform_data['existing_id']);
        $this->assertSame('config-app-id', $existing->platform_data['app_id']);
        $this->assertSame('keep-this-secret', $existing->platform_secrets['mcp_token']);
        $this->assertSame('config-app-secret', $existing->platform_secrets['app_secret']);
    }

    public function test_google_connection_reads_stored_credentials_when_config_is_empty(): void
    {
        PlatformConnection::factory()->google('1234567890', 'db-refresh-token')->create([
            'user_id' => null,
            'platform_data' => ['manager_customer_id' => '9876543210', 'client_id' => 'db-client-id'],
            'platform_secrets' => ['client_secret' => 'db-client-secret', 'developer_token' => 'db-developer-token'],
        ]);
        config([
            'services.google_ads.refresh_token' => null,
            'services.google_ads.customer_id' => null,
            'services.google_ads.login_customer_id' => null,
            'services.google_ads.oauth_client_id' => null,
            'services.google_ads.oauth_client_secret' => null,
            'services.google_ads.developer_token' => null,
            'google-ads.refresh_token' => null,
            'google-ads.customer_id' => null,
            'google-ads.login_customer_id' => null,
            'google-ads.client_id' => null,
            'google-ads.client_secret' => null,
            'google-ads.developer_token' => null,
        ]);

        $connection = GoogleAdsConnection::shared();

        $this->assertNotNull($connection);
        $this->assertSame('1234567890', $connection->customer_id);
        $this->assertSame('9876543210', $connection->manager_customer_id);
        $this->assertSame('db-refresh-token', $connection->refresh_token);
        $this->assertSame('db-client-id', GoogleAdsConnection::oauthClientId());
        $this->assertSame('db-client-secret', GoogleAdsConnection::oauthClientSecret());
        $this->assertSame('db-developer-token', GoogleAdsConnection::developerToken());
        $this->assertSame([], app(GoogleAdsClientFactory::class)->missingConfigurationKeys());
        $this->assertSame('1234567890', app(GoogleAdsClientFactory::class)->resolveCustomerId());
    }

    public function test_meta_and_linkedin_refresh_from_stored_credentials_without_config(): void
    {
        config([
            'platforms.meta.token' => null,
            'platforms.linkedin.token' => null,
            'platforms.linkedin.client_id' => null,
            'platforms.linkedin.client_secret' => null,
            'platforms.linkedin.ad_account_id' => null,
            'platforms.linkedin.organization_id' => null,
            'services.meta.client_id' => null,
            'services.meta.client_secret' => null,
        ]);
        $meta = PlatformConnection::factory()->meta('db-meta-token')->create([
            'user_id' => null,
            'platform_data' => ['app_id' => 'db-meta-app'],
            'platform_secrets' => ['app_secret' => 'db-meta-secret'],
        ]);
        $linkedin = PlatformConnection::factory()->linkedin()->create([
            'user_id' => null,
            'account_id' => 'db-ad-account',
            'platform_data' => ['client_id' => 'db-linkedin-client', 'organization_id' => 'db-organization'],
            'platform_secrets' => ['client_secret' => 'db-linkedin-secret'],
        ]);
        Http::fake([
            'graph.facebook.com/*' => Http::response(['access_token' => 'refreshed-meta-token', 'expires_in' => 3600]),
            'www.linkedin.com/oauth/v2/accessToken' => Http::response(['access_token' => 'refreshed-linkedin-token', 'expires_in' => 3600]),
        ]);

        app(MetaTokenRefresher::class)->refresh($meta);
        app(LinkedInTokenRefresher::class)->refresh($linkedin);

        $this->assertSame('db-ad-account', app(LinkedInRules::class)->defaultAdAccountId());
        $this->assertSame('db-organization', PlatformConnection::sharedMetadata('linkedin', 'organization_id'));
        Http::assertSent(fn ($request): bool => str_contains($request->url(), 'graph.facebook.com')
            && $request['client_id'] === 'db-meta-app'
            && $request['client_secret'] === 'db-meta-secret');
        Http::assertSent(fn ($request): bool => str_contains($request->url(), 'linkedin.com/oauth')
            && $request['client_id'] === 'db-linkedin-client'
            && $request['client_secret'] === 'db-linkedin-secret');
    }

    public function test_legacy_platform_data_is_split_without_exposing_secrets(): void
    {
        $legacyData = [
            'organization_id' => 'org-123',
            'ad_account_id' => 'account-456',
            'client_secret' => 'private-client-secret',
            'custom_credential' => 'private-unknown-secret',
        ];
        $connection = PlatformConnection::factory()->linkedin()->create();
        DB::table('platform_connections')->where('id', $connection->id)->update([
            'platform_data' => Crypt::encryptString(json_encode($legacyData, JSON_THROW_ON_ERROR)),
        ]);

        $migration = require database_path('migrations/2026_10_01_123256_separate_platform_connection_metadata_and_secrets.php');
        $migration->up();

        $connection->refresh();
        $this->assertSame(['organization_id' => 'org-123', 'ad_account_id' => 'account-456'], $connection->platform_data);
        $this->assertSame('private-client-secret', $connection->platform_secrets['client_secret']);
        $this->assertSame('private-unknown-secret', $connection->platform_secrets['custom_credential']);
        $this->assertStringNotContainsString('private-client-secret', (string) $connection->getRawOriginal('platform_data'));
    }

    public function test_legacy_platform_data_with_an_invalid_mac_is_left_untouched(): void
    {
        $connection = PlatformConnection::factory()->meta()->create();
        DB::table('platform_connections')->where('id', $connection->id)->update(['platform_data' => 'invalid-ciphertext']);

        $migration = require database_path('migrations/2026_10_01_123256_separate_platform_connection_metadata_and_secrets.php');

        try {
            $migration->up();
            $this->fail('Expected encrypted platform data to fail decryption.');
        } catch (DecryptException) {
            $this->assertSame('invalid-ciphertext', DB::table('platform_connections')->where('id', $connection->id)->value('platform_data'));
        }
    }
}
