<?php

namespace Tests\Feature;

use App\Http\Controllers\Admin\MansionSiteSyncController;
use App\Models\SiteSyncOutbox;
use App\Models\User;
use App\Services\CompanyApiService;
use App\Services\MansionSiteSyncService;
use App\Services\SiteSyncResult;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Mockery;
use Tests\TestCase;

class RetryMansionSiteSyncTest extends TestCase
{
    private const COMPANY_ID = 10;
    private const BUILDING_ID = 200;

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

        config()->set('database.default', 'sqlite');
        config()->set('database.connections.sqlite.database', ':memory:');
        DB::purge('sqlite');

        Schema::create('site_sync_outboxes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('company_id');
            $table->unsignedBigInteger('building_id');
            $table->string('operation', 64);
            $table->string('delivery_token', 64)->nullable();
            $table->longText('payload');
            $table->string('status');
            $table->unsignedInteger('retry_count')->default(0);
            $table->text('last_error')->nullable();
            $table->timestamp('next_retry_at')->nullable();
            $table->timestamp('processing_started_at')->nullable();
            $table->timestamp('synced_at')->nullable();
            $table->timestamps();
            $table->unique(['company_id', 'building_id', 'operation']);
        });
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('company_id');
            $table->unsignedBigInteger('building_id')->unique();
            $table->unsignedBigInteger('mansion_id')->nullable();
            $table->string('property')->nullable();
            $table->string('name')->nullable();
            $table->string('name_apartment')->nullable();
            $table->timestamps();
        });
        foreach (['relation_shareds', 'relation_lawrestrictions', 'relation_otherrestrictions'] as $tableName) {
            Schema::create($tableName, function (Blueprint $table) {
                $table->increments('id');
                $table->unsignedInteger('company_id');
                $table->unsignedBigInteger('article_id');
                $table->unsignedInteger('item_id');
            });
        }
        Schema::create('relation_event_days', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('company_id');
            $table->unsignedBigInteger('article_id');
            $table->date('event_day');
        });
    }

    protected function tearDown(): void
    {
        Carbon::setTestNow();
        parent::tearDown();
    }

    public function test_command_retries_only_due_pending_rows_and_claims_before_delivery(): void
    {
        Carbon::setTestNow(Carbon::parse('2026-09-04 10:00:00'));
        $due = $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Due mansion'], ['next_retry_at' => now()->subMinute()]);
        $otherCompanyDue = $this->queue(['company_id' => 20, 'name' => 'Other company due mansion'], [
            'company_id' => 20,
            'building_id' => 300,
            'next_retry_at' => now()->subMinute(),
        ]);
        $future = $this->queue(['name' => 'Future mansion'], [
            'building_id' => 201,
            'next_retry_at' => now()->addMinute(),
        ]);
        $failed = $this->queue(['name' => 'Manual only'], [
            'building_id' => 202,
            'status' => SiteSyncOutbox::STATUS_FAILED,
            'next_retry_at' => null,
        ]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('forCompany')->once()->with(self::COMPANY_ID)->andReturnSelf();
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $due->payload)
            ->andReturnUsing(function () use ($due) {
                $this->assertSame(
                    SiteSyncOutbox::STATUS_PROCESSING,
                    SiteSyncOutbox::findOrFail($due->id)->status
                );

                return SiteSyncResult::success(200);
            });
        $api->shouldReceive('forCompany')->once()->with(20)->andReturnSelf();
        $api->shouldReceive('editMansion')->once()->with(300, $otherCompanyDue->payload)
            ->andReturn(SiteSyncResult::success(200));
        app()->instance(CompanyApiService::class, $api);

        $this->assertSame(0, Artisan::call('site-sync:retry-mansions', ['--limit' => 50]));

        $due->refresh();
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $due->status);
        $this->assertNotNull($due->synced_at);
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $otherCompanyDue->fresh()->status);
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $future->fresh()->status);
        $this->assertSame(SiteSyncOutbox::STATUS_FAILED, $failed->fresh()->status);
    }

    public function test_command_applies_five_and_fifteen_minute_backoff_then_stops_automatically(): void
    {
        Carbon::setTestNow(Carbon::parse('2026-09-04 10:00:00'));
        $outbox = $this->queue(['name' => 'Retry mansion'], ['next_retry_at' => now()]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('forCompany')->times(3)->with(self::COMPANY_ID)->andReturnSelf();
        $api->shouldReceive('editMansion')->times(3)
            ->andReturn(SiteSyncResult::failure(503, 'secret upstream detail'));
        app()->instance(CompanyApiService::class, $api);

        Artisan::call('site-sync:retry-mansions');
        $outbox->refresh();
        $this->assertSame(1, $outbox->retry_count);
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $outbox->status);
        $this->assertSame(now()->addMinutes(5)->format('Y-m-d H:i:s'), $outbox->next_retry_at->format('Y-m-d H:i:s'));

        Carbon::setTestNow($outbox->next_retry_at);
        Artisan::call('site-sync:retry-mansions');
        $outbox->refresh();
        $this->assertSame(2, $outbox->retry_count);
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $outbox->status);
        $this->assertSame(now()->addMinutes(15)->format('Y-m-d H:i:s'), $outbox->next_retry_at->format('Y-m-d H:i:s'));

        Carbon::setTestNow($outbox->next_retry_at);
        Artisan::call('site-sync:retry-mansions');
        $outbox->refresh();
        $this->assertSame(3, $outbox->retry_count);
        $this->assertSame(SiteSyncOutbox::STATUS_FAILED, $outbox->status);
        $this->assertNull($outbox->next_retry_at);
        $this->assertSame('Site synchronization failed with HTTP status 503.', $outbox->last_error);
    }

    public function test_command_converts_an_unexpected_exception_to_failure_and_continues_the_batch(): void
    {
        Carbon::setTestNow(Carbon::parse('2026-09-04 10:00:00'));
        $first = $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Throws'], ['next_retry_at' => now()]);
        $second = $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Continues'], [
            'building_id' => 201,
            'next_retry_at' => now(),
        ]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('forCompany')->twice()->with(self::COMPANY_ID)->andReturnSelf();
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $first->payload)
            ->andThrow(new \RuntimeException('Authorization: Bearer must-not-leak'));
        $api->shouldReceive('editMansion')->once()->with(201, $second->payload)
            ->andReturn(SiteSyncResult::success(200));
        app()->instance(CompanyApiService::class, $api);

        Artisan::call('site-sync:retry-mansions');

        $first->refresh();
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $first->status);
        $this->assertSame(1, $first->retry_count);
        $this->assertSame('Site synchronization failed without an HTTP response.', $first->last_error);
        $this->assertNotContains('Bearer', $first->last_error);
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $second->fresh()->status);
    }

    public function test_command_reclaims_only_expired_processing_leases(): void
    {
        Carbon::setTestNow(Carbon::parse('2026-09-04 10:00:00'));
        $stale = $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Stale claim'], [
            'status' => SiteSyncOutbox::STATUS_PROCESSING,
            'processing_started_at' => now()->subMinutes(6),
            'next_retry_at' => null,
        ]);
        $fresh = $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Fresh claim'], [
            'building_id' => 201,
            'status' => SiteSyncOutbox::STATUS_PROCESSING,
            'processing_started_at' => now(),
            'next_retry_at' => null,
        ]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('forCompany')->once()->with(self::COMPANY_ID)->andReturnSelf();
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $stale->payload)
            ->andReturn(SiteSyncResult::success(200));
        app()->instance(CompanyApiService::class, $api);

        Artisan::call('site-sync:retry-mansions');

        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $stale->fresh()->status);
        $this->assertSame(SiteSyncOutbox::STATUS_PROCESSING, $fresh->fresh()->status);
    }

    public function test_manual_retry_uses_fresh_server_payload_and_sets_success_message(): void
    {
        DB::table('articles')->insert([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'mansion_id' => null,
            'property' => 6,
            'name' => 'Fresh server mansion',
            'name_apartment' => 'Fresh server mansion',
        ]);
        DB::table('relation_shareds')->insert([
            'company_id' => self::COMPANY_ID,
            'article_id' => self::BUILDING_ID,
            'item_id' => 7,
        ]);
        request()->merge(['payload' => ['name' => 'Browser supplied stale value']]);
        Auth::shouldReceive('check')->andReturn(false);
        Auth::shouldReceive('user')->andReturn((object) ['company_id' => self::COMPANY_ID]);
        $service = Mockery::mock(MansionSiteSyncService::class);
        $service->shouldReceive('sendOrQueueIfIdle')->once()->with(
            self::COMPANY_ID,
            self::BUILDING_ID,
            Mockery::on(function (array $payload): bool {
                $this->assertSame('Fresh server mansion', $payload['name']);
                $this->assertEquals([7], $payload['shared']);
                $this->assertArrayNotHasKey('payload', $payload);

                return true;
            })
        )->andReturn(SiteSyncResult::success(200));

        $response = (new MansionSiteSyncController())->retry(self::BUILDING_ID, $service);

        $this->assertSame(route('showEditMansion', ['id' => self::BUILDING_ID]), $response->getTargetUrl());
        $this->assertTrue(session()->has('site_sync_success'));
    }

    public function test_manual_retry_rejects_a_building_owned_by_another_company(): void
    {
        DB::table('articles')->insert([
            'company_id' => 11,
            'building_id' => self::BUILDING_ID,
            'mansion_id' => null,
            'property' => 6,
            'name' => 'Other company mansion',
        ]);
        $service = Mockery::mock(MansionSiteSyncService::class);
        $service->shouldNotReceive('sendOrQueueIfIdle');
        app()->instance(MansionSiteSyncService::class, $service);
        config()->set('database.connections.mysql_cp10', config('database.connections.sqlite'));
        DB::purge('mysql_cp10');
        DB::connection('mysql_cp10')->setPdo(DB::connection('sqlite')->getPdo());
        $user = new User(['id' => 1, 'company_id' => self::COMPANY_ID, 'name' => 'Tester']);
        $user->exists = true;
        $this->actingAs($user);

        $this->post(route('mansion.site-sync.retry', ['id' => self::BUILDING_ID]))
            ->assertStatus(404);
    }

    public function test_manual_retry_rejects_a_same_company_non_mansion(): void
    {
        DB::table('articles')->insert([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'mansion_id' => null,
            'property' => 1,
            'name' => 'House',
        ]);
        Auth::shouldReceive('check')->andReturn(false);
        Auth::shouldReceive('user')->andReturn((object) ['company_id' => self::COMPANY_ID]);
        $service = Mockery::mock(MansionSiteSyncService::class);
        $service->shouldNotReceive('sendOrQueueIfIdle');
        $this->expectException(ModelNotFoundException::class);

        (new MansionSiteSyncController())->retry(self::BUILDING_ID, $service);
    }

    public function test_manual_retry_does_not_dispatch_while_an_automatic_delivery_is_processing(): void
    {
        DB::table('articles')->insert([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'mansion_id' => null,
            'property' => 6,
            'name' => 'Fresh server mansion',
        ]);
        $this->queue(['company_id' => self::COMPANY_ID, 'name' => 'Claimed payload'], [
            'status' => SiteSyncOutbox::STATUS_PROCESSING,
            'processing_started_at' => now(),
            'next_retry_at' => null,
        ]);
        Auth::shouldReceive('check')->andReturn(false);
        Auth::shouldReceive('user')->andReturn((object) ['company_id' => self::COMPANY_ID]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldNotReceive('editMansion');

        $response = (new MansionSiteSyncController())->retry(
            self::BUILDING_ID,
            new MansionSiteSyncService($api)
        );

        $this->assertSame(route('showEditMansion', ['id' => self::BUILDING_ID]), $response->getTargetUrl());
        $this->assertTrue(session()->has('site_sync_warning'));
        $this->assertSame(SiteSyncOutbox::STATUS_PROCESSING, SiteSyncOutbox::firstOrFail()->status);
    }

    public function test_manual_retry_route_requires_authentication(): void
    {
        $this->post('/admin/mansion-site-sync/' . self::BUILDING_ID . '/retry')
            ->assertRedirect(route('login'));
    }

    public function test_retry_command_is_scheduled_every_minute_without_overlapping(): void
    {
        $events = array_values(array_filter(app(Schedule::class)->events(), function ($event): bool {
            return strpos($event->command, 'site-sync:retry-mansions --limit=50') !== false;
        }));

        $this->assertCount(1, $events);
        $this->assertSame('* * * * *', $events[0]->expression);
        $this->assertTrue($events[0]->withoutOverlapping);
        $this->assertSame(6, $events[0]->expiresAt);
    }

    private function queue(array $payload, array $overrides = []): SiteSyncOutbox
    {
        return SiteSyncOutbox::create(array_merge([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'operation' => 'edit_mansion',
            'delivery_token' => null,
            'payload' => $payload,
            'status' => SiteSyncOutbox::STATUS_PENDING,
            'retry_count' => 0,
            'last_error' => 'Earlier failure',
            'next_retry_at' => now(),
            'processing_started_at' => null,
            'synced_at' => null,
        ], $overrides));
    }
}
