HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/tests/Unit/RemoteSubscription/RemoteSubscriptionSyncServiceTest.php
<?php

namespace Tests\Unit\RemoteSubscription;

use Tests\TestCase;
use Mockery;
use App\Library\RemoteSubscriptionSyncService;
use App\Library\DTOs\RemoteSyncResult;
use App\Cashier\DTO\RemoteSubscriptionDTO;
use App\Cashier\DTO\RemotePlanDTO;
use App\Cashier\Contracts\IntentGatewayInterface;
use App\Cashier\Contracts\RemoteSubscriptionGatewayInterface;
use App\Model\Subscription;
use App\Model\PaymentGateway;
use App\Model\PlanRemoteMapping;
use App\Model\Plan;
use App\Library\Facades\Billing;
use App\Library\Facades\SubscriptionFacade;
use App\Services\Subscription\SubscriptionManagementService;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

class RemoteSubscriptionSyncServiceTest extends TestCase
{
    private RemoteSubscriptionSyncService $service;

    protected function setUp(): void
    {
        parent::setUp();
        $this->service = new RemoteSubscriptionSyncService();
    }

    // ==========================================
    // syncSubscription tests
    // ==========================================

    public function test_sync_returns_error_when_no_remote_subscription()
    {
        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(false);

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_ERROR, $result->status);
        $this->assertStringContainsString('No remote subscription', $result->error);
    }

    public function test_sync_returns_error_when_gateway_not_remote()
    {
        // IntentGatewayInterface (required by PaymentGateway::getService() return type)
        // but NOT RemoteSubscriptionGatewayInterface — exercises the "no remote support" guard.
        $nonRemoteService = Mockery::mock(IntentGatewayInterface::class);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($nonRemoteService);

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->remoteGateway = $gateway;

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_ERROR, $result->status);
        $this->assertStringContainsString('does not support remote subscriptions', $result->error);
    }

    public function test_sync_returns_in_sync_when_no_changes()
    {
        $periodEnd = Carbon::parse('2025-09-01 00:00:00');
        $remotePlanId = 'price_abc123';

        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: $remotePlanId,
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: $periodEnd->copy(),
            currentPeriodStart: Carbon::parse('2025-08-01 00:00:00'),
            canceledAt: null,
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->with('sub_test')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Test Stripe Gateway';
        $gateway->type = 'stripe-subscription';

        // Use attribute map to avoid Eloquent getAttribute/setAttribute issues with Mockery
        $attrs = [
            'remoteGateway' => $gateway,
            'plan' => null,
            'status' => 'active',
            'uid' => 'sub_uid_123',
            'remote_subscription_id' => 'sub_test',
            'current_period_ends_at' => $periodEnd->copy(),
            'plan_id' => null,
            'remote_metadata' => [
                'remote_plan_id' => $remotePlanId,
                'latest_invoice_amount' => 29.99,
            ],
            'cancelled_at' => null,
            'last_synced_at' => null,
        ];

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        // Route all attribute access through our map
        $subscription->shouldReceive('getAttribute')->andReturnUsing(function ($key) use (&$attrs) {
            return $attrs[$key] ?? null;
        });
        $subscription->shouldReceive('setAttribute')->andReturnUsing(function ($key, $value) use (&$attrs, $subscription) {
            $attrs[$key] = $value;
            return $subscription;
        });

        // Allow direct property access for Mockery
        $subscription->uid = 'sub_uid_123';

        // Mock SubscriptionFacade::log
        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_IN_SYNC, $result->status);
        $this->assertEmpty($result->changes);
    }

    public function test_sync_detects_cancelled_remote_subscription()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'canceled',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(15),
            currentPeriodStart: Carbon::now()->subDays(15),
            canceledAt: Carbon::now()->subHours(2),
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('setCancelled')->once();
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_cancel';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(15);
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_abc'];

        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('cancelled_remote', $result->changes['status']['new']);
    }

    public function test_sync_detects_past_due_remote()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'past_due',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(5),
            currentPeriodStart: Carbon::now()->subDays(25),
            canceledAt: null,
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'open',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_pd';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(5);
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_abc', 'latest_invoice_amount' => 29.99];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('warning')->zeroOrMoreTimes();
        Log::shouldReceive('info')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        // Past due now records a status change (drifted)
        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('past_due_remote', $result->changes['status']['new']);
        $this->assertTrue($result->hasWarnings());
        $this->assertEquals(Subscription::REMOTE_RECONCILE_STATUS_STATE_MISMATCH, $subscription->remote_reconcile_status);
        $warningTexts = implode(' ', $result->warnings);
        $this->assertStringContainsString('past_due', $warningTexts);
    }

    public function test_sync_returns_remote_not_found_on_api_exception()
    {
        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')
            ->andThrow(new \Exception('Subscription not found in Stripe'));

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->uid = 'sub_uid_nf';
        $subscription->remote_subscription_id = 'sub_nonexistent';

        // logSyncError uses both Log::error and SubscriptionFacade::log
        Log::shouldReceive('error')->once();
        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_REMOTE_NOT_FOUND, $result->status);
        $this->assertStringContainsString('Subscription not found in Stripe', $result->error);
        $this->assertEquals(Subscription::REMOTE_RECONCILE_STATUS_NOT_FOUND, $subscription->remote_reconcile_status);
    }

    public function test_sync_returns_error_on_non_not_found_api_exception()
    {
        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')
            ->andThrow(new \Exception('API rate limit exceeded'));

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->uid = 'sub_uid_sync_err';
        $subscription->remote_subscription_id = 'sub_rate_limited';

        Log::shouldReceive('error')->once();
        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_ERROR, $result->status);
        $this->assertStringContainsString('API rate limit exceeded', $result->error);
        $this->assertEquals(Subscription::REMOTE_RECONCILE_STATUS_SYNC_FAILED, $subscription->remote_reconcile_status);
    }

    public function test_sync_detects_period_end_drift()
    {
        $localPeriodEnd = Carbon::parse('2025-08-01 00:00:00');
        $remotePeriodEnd = Carbon::parse('2025-09-01 00:00:00');

        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: $remotePeriodEnd,
            currentPeriodStart: Carbon::now(),
            canceledAt: null,
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);
        $mockService->shouldReceive('getRemoteInvoices')->andReturn(['data' => [], 'has_more' => false]);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_period';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = $localPeriodEnd;
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_abc'];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('current_period_ends_at', $result->changes);
        $this->assertEquals($remotePeriodEnd->toDateTimeString(), $result->changes['current_period_ends_at']['new']);
    }

    public function test_sync_detects_plan_change()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: 'price_new_plan',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(25),
            currentPeriodStart: Carbon::now()->subDays(5),
            canceledAt: null,
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);
        $mockService->shouldReceive('getRemoteInvoices')->andReturn(['data' => [], 'has_more' => false]);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_planchange';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(25);
        $subscription->plan_id = null;
        $subscription->remote_metadata = [
            'remote_plan_id' => 'price_old_plan', // differs from remote
        ];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('remote_plan_id', $result->changes);
        $this->assertEquals('price_old_plan', $result->changes['remote_plan_id']['old']);
        $this->assertEquals('price_new_plan', $result->changes['remote_plan_id']['new']);
    }

    public function test_sync_detects_invoice_amount_change()
    {
        $periodEnd = Carbon::now()->addDays(20);

        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: $periodEnd,
            currentPeriodStart: Carbon::now()->subDays(10),
            canceledAt: null,
            latestInvoiceAmount: 49.99, // changed from 29.99
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);
        $mockService->shouldReceive('getRemoteInvoices')->andReturn(['data' => [], 'has_more' => false]);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_invoice';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = $periodEnd;
        $subscription->plan_id = null;
        $subscription->remote_metadata = [
            'remote_plan_id' => 'price_abc',
            'latest_invoice_amount' => 29.99, // old amount
        ];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->once();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('latest_invoice_amount', $result->changes);
        $this->assertEquals(29.99, $result->changes['latest_invoice_amount']['old']);
        $this->assertEquals(49.99, $result->changes['latest_invoice_amount']['new']);
    }

    // ==========================================
    // syncPlanMapping tests
    // ==========================================

    public function test_sync_plan_mapping_updated()
    {
        $remotePlan = new RemotePlanDTO(
            id: 'price_abc',
            name: 'Updated Plan Name',
            price: 39.99,
            currency: 'USD',
            intervalCount: 1,
            intervalUnit: 'month',
            status: 'active',
            trialDays: 7,
        );

        $mockGatewayService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockGatewayService->shouldReceive('getRemotePlan')->with('price_abc')->andReturn($remotePlan);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockGatewayService);
        $gateway->name = 'Test GW';
        $gateway->type = 'stripe-subscription';

        $currency = Mockery::mock();
        $currency->code = 'USD';

        $plan = Mockery::mock(Plan::class)->makePartial();
        $plan->price = 39.99;
        $plan->frequency_amount = '1';
        $plan->frequency_unit = 'month';
        $plan->trial_amount = 7;
        $plan->trial_unit = 'day';
        $plan->name = 'Test Plan';
        $plan->shouldReceive('getAttribute')->with('currency')->andReturn($currency);
        $plan->shouldReceive('hasTrial')->andReturn(true);

        $mapping = Mockery::mock(PlanRemoteMapping::class)->makePartial();
        $mapping->shouldReceive('getService')->andReturn($mockGatewayService);
        $mapping->shouldReceive('syncFromRemote')->with($remotePlan)->once();
        $mapping->shouldReceive('getMismatches')->andReturn([]);
        $mapping->shouldReceive('getComparisonSummary')->andReturn(['in_sync' => true]);
        $mapping->shouldReceive('getAttribute')->with('plan')->andReturn($plan);
        $mapping->shouldReceive('getAttribute')->with('paymentGateway')->andReturn($gateway);

        $mapping->uid = 'mapping_uid_1';
        $mapping->remote_plan_id = 'price_abc';
        $mapping->remote_plan_name = 'Old Plan Name'; // will change
        $mapping->remote_price = 29.99; // will change
        $mapping->remote_currency = 'USD';
        $mapping->remote_interval_count = 1;
        $mapping->remote_interval_unit = 'month';
        $mapping->remote_trial_days = 0; // will change

        $result = $this->service->syncPlanMapping($mapping);

        $this->assertEquals('updated', $result['status']);
        $this->assertNotEmpty($result['changes']);
        $this->assertArrayHasKey('name', $result['changes']);
        $this->assertArrayHasKey('price', $result['changes']);
        $this->assertArrayHasKey('trial_days', $result['changes']);
    }

    public function test_sync_plan_mapping_in_sync()
    {
        $remotePlan = new RemotePlanDTO(
            id: 'price_abc',
            name: 'Same Name',
            price: 29.99,
            currency: 'USD',
            intervalCount: 1,
            intervalUnit: 'month',
            status: 'active',
            trialDays: 0,
        );

        $mockGatewayService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockGatewayService->shouldReceive('getRemotePlan')->with('price_abc')->andReturn($remotePlan);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        $gateway->name = 'Test GW';
        $gateway->type = 'stripe-subscription';

        $currency = Mockery::mock();
        $currency->code = 'USD';

        $plan = Mockery::mock(Plan::class)->makePartial();
        $plan->price = 29.99;
        $plan->frequency_amount = '1';
        $plan->frequency_unit = 'month';
        $plan->trial_amount = null;
        $plan->trial_unit = null;
        $plan->name = 'Test Plan';
        $plan->shouldReceive('getAttribute')->with('currency')->andReturn($currency);
        $plan->shouldReceive('hasTrial')->andReturn(false);

        $mapping = Mockery::mock(PlanRemoteMapping::class)->makePartial();
        $mapping->shouldReceive('getService')->andReturn($mockGatewayService);
        $mapping->shouldReceive('syncFromRemote')->once();
        $mapping->shouldReceive('getMismatches')->andReturn([]);
        $mapping->shouldReceive('getComparisonSummary')->andReturn(['in_sync' => true]);
        $mapping->shouldReceive('getAttribute')->with('plan')->andReturn($plan);
        $mapping->shouldReceive('getAttribute')->with('paymentGateway')->andReturn($gateway);

        $mapping->uid = 'mapping_uid_2';
        $mapping->remote_plan_id = 'price_abc';
        $mapping->remote_plan_name = 'Same Name';
        $mapping->remote_price = 29.99;
        $mapping->remote_currency = 'USD';
        $mapping->remote_interval_count = 1;
        $mapping->remote_interval_unit = 'month';
        $mapping->remote_trial_days = 0;

        $result = $this->service->syncPlanMapping($mapping);

        $this->assertEquals('in_sync', $result['status']);
        $this->assertEmpty($result['changes']);
    }

    public function test_sync_plan_mapping_handles_api_error()
    {
        $mockGatewayService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockGatewayService->shouldReceive('getRemotePlan')
            ->andThrow(new \Exception('API rate limit exceeded'));

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        $gateway->name = 'Test GW';

        $mapping = Mockery::mock(PlanRemoteMapping::class)->makePartial();
        $mapping->shouldReceive('getService')->andReturn($mockGatewayService);
        $mapping->shouldReceive('getAttribute')->with('paymentGateway')->andReturn($gateway);

        $mapping->uid = 'mapping_uid_3';
        $mapping->remote_plan_id = 'price_invalid';

        Log::shouldReceive('error')->once();

        $result = $this->service->syncPlanMapping($mapping);

        $this->assertEquals('error', $result['status']);
        $this->assertStringContainsString('API rate limit exceeded', $result['error']);
    }

    public function test_sync_plan_mapping_warns_on_mismatch_after_sync()
    {
        $remotePlan = new RemotePlanDTO(
            id: 'price_abc',
            name: 'Remote Plan',
            price: 49.99, // different from local plan's $29.99
            currency: 'USD',
            intervalCount: 1,
            intervalUnit: 'month',
            status: 'active',
        );

        $mockGatewayService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockGatewayService->shouldReceive('getRemotePlan')->andReturn($remotePlan);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        $gateway->name = 'Test GW';
        $gateway->type = 'stripe-subscription';

        $mismatches = [
            ['field' => 'price', 'local' => 29.99, 'remote' => 49.99, 'message' => 'Price mismatch: local 29.99 vs remote 49.99'],
        ];

        $mapping = Mockery::mock(PlanRemoteMapping::class)->makePartial();
        $mapping->shouldReceive('getService')->andReturn($mockGatewayService);
        $mapping->shouldReceive('syncFromRemote')->once();
        $mapping->shouldReceive('getMismatches')->andReturn($mismatches);
        $mapping->shouldReceive('getComparisonSummary')->andReturn(['in_sync' => false]);
        $mapping->shouldReceive('getAttribute')->with('plan')->andReturn((object) ['name' => 'Local Plan']);
        $mapping->shouldReceive('getAttribute')->with('paymentGateway')->andReturn($gateway);

        $mapping->uid = 'mapping_uid_4';
        $mapping->remote_plan_id = 'price_abc';
        $mapping->remote_plan_name = 'Remote Plan';
        $mapping->remote_price = 49.99; // after sync
        $mapping->remote_currency = 'USD';
        $mapping->remote_interval_count = 1;
        $mapping->remote_interval_unit = 'month';
        $mapping->remote_trial_days = 0;

        Log::shouldReceive('warning')->once();

        $result = $this->service->syncPlanMapping($mapping);

        $this->assertNotEmpty($result['mismatches']);
        $this->assertEquals('price', $result['mismatches'][0]['field']);
    }

    // ==========================================
    // findGateway tests
    // ==========================================

    public function test_find_gateway_throws_when_not_found()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessageMatches('/not found/');

        $this->service->findGateway('nonexistent_uid');
    }

    // ==========================================
    // DTO integration within sync
    // ==========================================

    public function test_sync_stores_all_remote_metadata()
    {
        $periodEnd = Carbon::parse('2025-09-01 00:00:00');
        $periodStart = Carbon::parse('2025-08-01 00:00:00');
        $canceledAt = null;

        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_detailed',
            status: 'active',
            remotePlanId: 'price_xyz',
            remoteCustomerId: 'cus_detailed',
            currentPeriodEnd: $periodEnd,
            currentPeriodStart: $periodStart,
            canceledAt: $canceledAt,
            latestInvoiceAmount: 99.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe Prod';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_meta';
        $subscription->remote_subscription_id = 'sub_detailed';
        $subscription->current_period_ends_at = $periodEnd;
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_xyz'];

        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        // Verify metadata was stored
        $meta = $subscription->remote_metadata;
        $this->assertEquals('active', $meta['remote_status']);
        $this->assertEquals('price_xyz', $meta['remote_plan_id']);
        $this->assertEquals('cus_detailed', $meta['remote_customer_id']);
        $this->assertEquals($periodEnd->toDateTimeString(), $meta['remote_period_end']);
        $this->assertEquals($periodStart->toDateTimeString(), $meta['remote_period_start']);
        $this->assertEquals(99.99, $meta['latest_invoice_amount']);
        $this->assertEquals('paid', $meta['latest_invoice_status']);
        $this->assertNotNull($subscription->last_synced_at);
    }

    // ==========================================
    // New status case tests
    // ==========================================

    public function test_sync_activates_new_subscription_when_remote_active()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(30),
            currentPeriodStart: Carbon::now(),
            canceledAt: null,
            latestInvoiceAmount: 50.00,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);
        $mockService->shouldReceive('getRemoteInvoices')->andReturn(['data' => [], 'has_more' => false]);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(true);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'new';
        $subscription->uid = 'sub_uid_activate';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = null;
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('activateFromRemote')->with($subscription, $gateway)->once()->andReturn(true);
        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('activated_from_sync', $result->changes['status']['new']);
    }

    public function test_sync_detects_incomplete_expired_for_new_subscription()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'incomplete_expired',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: null,
            currentPeriodStart: null,
            canceledAt: null,
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(true);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'new';
        $subscription->uid = 'sub_uid_inc_exp';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = null;
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();
        Log::shouldReceive('warning')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('incomplete_expired', $result->changes['status']['new']);
        $warningTexts = implode(' ', $result->warnings);
        $this->assertStringContainsString('expired', $warningTexts);
    }

    public function test_sync_handles_paused_remote_for_active_local()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'paused',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(10),
            currentPeriodStart: Carbon::now()->subDays(20),
            canceledAt: null,
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_paused';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(10);
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();
        Log::shouldReceive('warning')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $warningTexts = implode(' ', $result->warnings);
        $this->assertStringContainsString('paused', $warningTexts);
    }

    public function test_sync_incomplete_expired_for_active_local()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'incomplete_expired',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(5),
            currentPeriodStart: Carbon::now()->subDays(25),
            canceledAt: null,
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'active';
        $subscription->uid = 'sub_uid_ie_active';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(5);
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();
        Log::shouldReceive('warning')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('incomplete_expired', $result->changes['status']['new']);
    }

    // ==========================================
    // Cancelled subscription sync tests (Phase 6)
    // ==========================================

    public function test_sync_cancelled_sub_with_active_remote_reports_reactivated()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'active',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(15),
            currentPeriodStart: Carbon::now()->subDays(15),
            canceledAt: null,
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isCancelled')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'cancelled';
        $subscription->uid = 'sub_uid_react';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = Carbon::now()->addDays(15);
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_abc', 'latest_invoice_amount' => 29.99];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();
        Log::shouldReceive('warning')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('reactivated_remote', $result->changes['status']['new']);
        $this->assertTrue($result->hasWarnings());
    }

    public function test_sync_cancelled_sub_with_cancelled_remote_is_in_sync()
    {
        $periodEnd = Carbon::parse('2025-09-15 00:00:00');

        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'canceled',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: $periodEnd->copy(),
            currentPeriodStart: Carbon::parse('2025-08-15 00:00:00'),
            canceledAt: Carbon::parse('2025-09-10 00:00:00'),
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(false);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isCancelled')->andReturn(true);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'cancelled';
        $subscription->uid = 'sub_uid_both_cancel';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = $periodEnd->copy();
        $subscription->plan_id = null;
        $subscription->remote_metadata = ['remote_plan_id' => 'price_abc', 'latest_invoice_amount' => 29.99];

        SubscriptionFacade::shouldReceive('log')->once();

        $result = $this->service->syncSubscription($subscription);

        // Both cancelled, no period drift → in_sync
        $this->assertEquals(RemoteSyncResult::STATUS_IN_SYNC, $result->status);
        $this->assertEmpty($result->changes);
    }

    public function test_sync_new_sub_with_active_remote_activates()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_new_act',
            status: 'active',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(30),
            currentPeriodStart: Carbon::now(),
            canceledAt: null,
            latestInvoiceAmount: 29.99,
            latestInvoiceStatus: 'paid',
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);
        $mockService->shouldReceive('getRemoteInvoices')->andReturn(['data' => [], 'has_more' => false]);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(true);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'new';
        $subscription->uid = 'sub_uid_new_act';
        $subscription->remote_subscription_id = 'sub_new_act';
        $subscription->current_period_ends_at = null;
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('activateFromRemote')->with($subscription, $gateway)->once()->andReturn(true);
        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('activated_from_sync', $result->changes['status']['new']);
    }

    public function test_sync_new_sub_with_cancelled_remote_warns()
    {
        $remoteDTO = new RemoteSubscriptionDTO(
            id: 'sub_test',
            status: 'canceled',
            remotePlanId: 'price_abc',
            remoteCustomerId: 'cus_test',
            currentPeriodEnd: Carbon::now()->addDays(5),
            currentPeriodStart: Carbon::now()->subDays(25),
            canceledAt: Carbon::now()->subDays(1),
            latestInvoiceAmount: null,
            latestInvoiceStatus: null,
        );

        $mockService = Mockery::mock(IntentGatewayInterface::class, RemoteSubscriptionGatewayInterface::class);
        $mockService->shouldReceive('getRemoteSubscription')->andReturn($remoteDTO);

        $gateway = Mockery::mock(PaymentGateway::class)->makePartial();
        Billing::shouldReceive('resolveService')->andReturn($mockService);
        $gateway->id = 1;
        $gateway->name = 'Stripe GW';
        $gateway->type = 'stripe-subscription';

        $subscription = Mockery::mock(Subscription::class)->makePartial();
        $subscription->shouldReceive('hasRemoteSubscription')->andReturn(true);
        $subscription->shouldReceive('getAttribute')->with('remoteGateway')->andReturn($gateway);
        $subscription->shouldReceive('getAttribute')->with('plan')->andReturn(null);
        $subscription->shouldReceive('isNew')->andReturn(true);
        $subscription->shouldReceive('isActive')->andReturn(false);
        $subscription->shouldReceive('isEnded')->andReturn(false);
        $subscription->shouldReceive('save')->once();

        $subscription->remoteGateway = $gateway;
        $subscription->status = 'new';
        $subscription->uid = 'sub_uid_new_canc';
        $subscription->remote_subscription_id = 'sub_test';
        $subscription->current_period_ends_at = null;
        $subscription->plan_id = null;
        $subscription->remote_metadata = [];

        SubscriptionFacade::shouldReceive('log')->once();
        Log::shouldReceive('info')->zeroOrMoreTimes();
        Log::shouldReceive('warning')->zeroOrMoreTimes();

        $result = $this->service->syncSubscription($subscription);

        $this->assertEquals(RemoteSyncResult::STATUS_DRIFTED, $result->status);
        $this->assertArrayHasKey('status', $result->changes);
        $this->assertEquals('cancelled_remote', $result->changes['status']['new']);
        $this->assertTrue($result->hasWarnings());
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }
}