<?php

namespace Tests\Feature;

use App\Http\Controllers\Admin\ArticleController;
use App\Http\Requests\MansionPost;
use App\Models\SiteSyncOutbox;
use App\Services\CompanyApiService;
use App\Services\MansionRoomSynchronizer;
use App\Services\MansionSiteSyncService;
use App\Services\SiteSyncResult;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Mockery;
use Tests\TestCase;

class MansionSiteSyncOutboxTest extends TestCase
{
    private const COMPANY_ID = 10;
    private const BUILDING_ID = 200;
    private const OPERATION = 'edit_mansion';

    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'],
                'site_sync_outboxes_company_building_operation_unique'
            );
        });
        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->string('address1')->nullable();
            $table->string('address2')->nullable();
            $table->string('address3')->nullable();
            $table->string('address4')->nullable();
            $table->unsignedInteger('construction_sub')->nullable();
            $table->unsignedInteger('disp')->nullable();
            $table->unsignedInteger('first')->nullable();
            $table->string('del')->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')->nullable();
                $table->unsignedBigInteger('article_id');
                $table->unsignedInteger('item_id');
            });
        }
        Schema::create('relation_event_days', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('company_id')->nullable();
            $table->unsignedBigInteger('article_id');
            $table->date('event_day');
        });
    }

    public function test_a_first_time_success_creates_no_pending_outbox_row(): void
    {
        $payload = $this->payload('First success');
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $payload)
            ->andReturn(SiteSyncResult::success(201));

        $result = (new MansionSiteSyncService($api))
            ->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $payload);

        $this->assertTrue($result->isSuccessful());
        $this->assertSame(0, SiteSyncOutbox::where('status', SiteSyncOutbox::STATUS_PENDING)->count());
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, SiteSyncOutbox::firstOrFail()->status);
    }

    public function test_a_failure_queues_the_exact_replay_payload_without_credentials(): void
    {
        $payload = $this->payload('Failed mansion', [
            'shared' => [1, 2],
            'event_day' => ['2026-09-03'],
        ]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $payload)
            ->andReturn(SiteSyncResult::failure(503, 'Authorization: Bearer should-not-be-persisted'));

        DB::flushQueryLog();
        DB::enableQueryLog();
        (new MansionSiteSyncService($api))
            ->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $payload);
        $outboxQueries = array_values(array_filter(DB::getQueryLog(), function (array $query): bool {
            return strpos($query['query'], 'site_sync_outboxes') !== false;
        }));
        DB::disableQueryLog();

        $outbox = SiteSyncOutbox::firstOrFail();
        $this->assertCount(2, $outboxQueries, 'A delivery must register its generation and finalize it without a SELECT.');
        $this->assertStringStartsWith('insert', strtolower($outboxQueries[0]['query']));
        $this->assertStringStartsWith('update', strtolower($outboxQueries[1]['query']));
        $this->assertSame(self::COMPANY_ID, $outbox->company_id);
        $this->assertSame(self::BUILDING_ID, $outbox->building_id);
        $this->assertSame(self::OPERATION, $outbox->operation);
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $outbox->status);
        $this->assertSame(0, $outbox->retry_count);
        $this->assertSame($payload, $outbox->payload);
        $this->assertSame('Site synchronization failed with HTTP status 503.', $outbox->last_error);
        $this->assertNotContains('Authorization', $outbox->last_error);
        $this->assertNotContains('Bearer', $outbox->last_error);
        $this->assertNotContains('should-not-be-persisted', $outbox->last_error);
        $this->assertNotNull($outbox->next_retry_at);
        $this->assertTrue($outbox->next_retry_at->between(now()->addSeconds(55), now()->addSeconds(65)));
        $this->assertNull($outbox->synced_at);
    }

    public function test_a_later_failure_refreshes_the_existing_pending_record(): void
    {
        $firstPayload = $this->payload('First attempt');
        $latestPayload = $this->payload('Latest attempt', ['gas' => null]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('editMansion')->twice()->andReturn(
            SiteSyncResult::failure(500, 'first failure'),
            SiteSyncResult::failure(502, 'latest failure')
        );
        $service = new MansionSiteSyncService($api);

        $service->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $firstPayload);
        $service->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $latestPayload);

        $this->assertSame(1, SiteSyncOutbox::count());
        $outbox = SiteSyncOutbox::firstOrFail();
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $outbox->status);
        $this->assertSame(0, $outbox->retry_count);
        $this->assertSame($latestPayload, $outbox->payload);
        $this->assertSame('Site synchronization failed with HTTP status 502.', $outbox->last_error);
    }

    public function test_a_later_success_marks_an_older_pending_record_succeeded(): void
    {
        $oldPayload = $this->payload('Failed mansion');
        SiteSyncOutbox::create([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'operation' => self::OPERATION,
            'delivery_token' => 'older-delivery',
            'payload' => $oldPayload,
            'status' => SiteSyncOutbox::STATUS_FAILED,
            'retry_count' => 3,
            'last_error' => 'Old failure',
            'next_retry_at' => now()->addMinutes(5),
        ]);
        $successfulPayload = $this->payload('Recovered mansion');
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $successfulPayload)
            ->andReturn(SiteSyncResult::success(201));

        (new MansionSiteSyncService($api))
            ->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $successfulPayload);

        $this->assertSame(1, SiteSyncOutbox::count());
        $outbox = SiteSyncOutbox::firstOrFail();
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $outbox->status);
        $this->assertSame($successfulPayload, $outbox->payload);
        $this->assertNull($outbox->last_error);
        $this->assertNull($outbox->next_retry_at);
        $this->assertNotNull($outbox->synced_at);
    }

    public function test_an_older_failure_cannot_override_a_newer_success(): void
    {
        $olderPayload = $this->payload('Older failed save');
        $newerPayload = $this->payload('Newer successful save');
        $newerApi = Mockery::mock(CompanyApiService::class);
        $newerApi->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $newerPayload)
            ->andReturn(SiteSyncResult::success(201));
        $newerService = new MansionSiteSyncService($newerApi);
        $olderApi = Mockery::mock(CompanyApiService::class);
        $olderApi->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $olderPayload)
            ->andReturnUsing(function () use ($newerService, $newerPayload) {
                $newerService->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $newerPayload);

                return SiteSyncResult::failure(503, 'older failure');
            });

        (new MansionSiteSyncService($olderApi))
            ->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $olderPayload);

        $outbox = SiteSyncOutbox::firstOrFail();
        $this->assertSame(SiteSyncOutbox::STATUS_SUCCEEDED, $outbox->status);
        $this->assertSame($newerPayload, $outbox->payload);
        $this->assertNull($outbox->last_error);
    }

    public function test_an_older_success_cannot_override_a_newer_failure(): void
    {
        $olderPayload = $this->payload('Older successful save');
        $newerPayload = $this->payload('Newer failed save');
        $newerApi = Mockery::mock(CompanyApiService::class);
        $newerApi->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $newerPayload)
            ->andReturn(SiteSyncResult::failure(503, 'newer failure'));
        $newerService = new MansionSiteSyncService($newerApi);
        $olderApi = Mockery::mock(CompanyApiService::class);
        $olderApi->shouldReceive('editMansion')->once()->with(self::BUILDING_ID, $olderPayload)
            ->andReturnUsing(function () use ($newerService, $newerPayload) {
                $newerService->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $newerPayload);

                return SiteSyncResult::success(201);
            });

        (new MansionSiteSyncService($olderApi))
            ->sendOrQueue(self::COMPANY_ID, self::BUILDING_ID, $olderPayload);

        $outbox = SiteSyncOutbox::firstOrFail();
        $this->assertSame(SiteSyncOutbox::STATUS_PENDING, $outbox->status);
        $this->assertSame($newerPayload, $outbox->payload);
        $this->assertSame('Site synchronization failed with HTTP status 503.', $outbox->last_error);
    }

    public function test_controller_keeps_local_changes_and_flashes_warning_when_site_sync_fails(): void
    {
        [$response, $request] = $this->invokeController([], SiteSyncResult::failure(503, 'site unavailable'));

        $this->assertSame('admin.pages.redirect', $response->getName());
        $this->assertDatabaseHas('articles', [
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'name' => 'Saved mansion',
        ]);
        $this->assertDatabaseHas('articles', [
            'company_id' => self::COMPANY_ID,
            'building_id' => 201,
            'mansion_id' => self::BUILDING_ID,
            'name_apartment' => 'Saved mansion',
        ]);
        $this->assertDatabaseHas('site_sync_outboxes', [
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'status' => SiteSyncOutbox::STATUS_PENDING,
        ]);
        $this->assertTrue($request->session()->has('site_sync_warning'));
    }

    public function test_controller_does_not_flash_warning_when_site_sync_succeeds(): void
    {
        [, $request] = $this->invokeController([], SiteSyncResult::success(200));

        $this->assertFalse($request->session()->has('site_sync_warning'));
    }

    /**
     * @dataProvider redirectDestinationProvider
     */
    public function test_controller_preserves_redirect_destinations_and_warning(array $input, string $expectedPath): void
    {
        [$response, $request] = $this->invokeController($input, SiteSyncResult::failure(503, 'site unavailable'));

        $this->assertSame(url($expectedPath), $response->getTargetUrl());
        $this->assertTrue($request->session()->has('site_sync_warning'));
    }

    public function redirectDestinationProvider(): array
    {
        return [
            'image editor' => [['toimage' => 1], '/admin/registImage/' . self::BUILDING_ID],
            'existing room editor' => [['toroom' => 201], '/admin/editRoom/' . self::BUILDING_ID . '/201'],
            'new room form' => [['room' => 1], '/admin/registRoom/' . self::BUILDING_ID],
        ];
    }

    private function invokeController(array $additionalInput, SiteSyncResult $siteResult): array
    {
        DB::table('articles')->insert([
            [
                'company_id' => self::COMPANY_ID,
                'building_id' => self::BUILDING_ID,
                'mansion_id' => null,
                'property' => 6,
                'name' => 'Original mansion',
                'name_apartment' => 'Original mansion',
            ],
            [
                'company_id' => self::COMPANY_ID,
                'building_id' => 201,
                'mansion_id' => self::BUILDING_ID,
                'property' => null,
                'name' => 'Room 201',
                'name_apartment' => 'Original mansion',
            ],
        ]);
        Auth::shouldReceive('check')->andReturn(false);
        Auth::shouldReceive('user')->once()->andReturn((object) ['company_id' => self::COMPANY_ID]);
        $api = Mockery::mock(CompanyApiService::class);
        $api->shouldReceive('editMansion')->once()->andReturn($siteResult);
        $controller = Mockery::mock(ArticleController::class)->makePartial();
        $controller->shouldReceive('setCount')->zeroOrMoreTimes();
        $request = MansionPost::create(
            '/admin/editMansionSave/' . self::BUILDING_ID,
            'POST',
            array_merge([
                'id' => self::BUILDING_ID,
                'name' => 'Saved mansion',
            ], $additionalInput)
        );
        $session = app('session')->driver();
        $session->start();
        $request->setLaravelSession($session);

        $response = $controller->editMansion(
            $request,
            new MansionSiteSyncService($api),
            new MansionRoomSynchronizer()
        );

        return [$response, $request];
    }

    private function payload(string $name, array $additional = []): array
    {
        return array_merge([
            'company_id' => self::COMPANY_ID,
            'building_id' => self::BUILDING_ID,
            'name' => $name,
        ], $additional);
    }
}
