<?php

declare(strict_types=1);

namespace Tests\Unit\Services\Project;

use App\Models\Project;
use App\Services\Project\SupplierSnapshotGuard;
use Carbon\CarbonImmutable;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * Unit-тесты для SupplierSnapshotGuard.
 *
 * Spec: docs/superpowers/plans/2026-05-26-supplier-snapshot-guard.md
 */
class SupplierSnapshotGuardTest extends TestCase
{
    private SupplierSnapshotGuard $guard;

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

    public function test_grace_until_for_pause_before_21_msk_is_next_day_21_msk(): void
    {
        $pausedAt = CarbonImmutable::parse('2026-05-25 14:00:00', 'Europe/Moscow');
        $graceUntil = $this->guard->computeGraceUntil($pausedAt);

        $this->assertSame(
            '2026-05-26 21:00:00',
            $graceUntil->setTimezone('Europe/Moscow')->format('Y-m-d H:i:s'),
        );
    }

    public function test_grace_until_for_pause_after_21_msk_is_day_plus_two_21_msk(): void
    {
        $pausedAt = CarbonImmutable::parse('2026-05-25 22:00:00', 'Europe/Moscow');
        $graceUntil = $this->guard->computeGraceUntil($pausedAt);

        $this->assertSame(
            '2026-05-27 21:00:00',
            $graceUntil->setTimezone('Europe/Moscow')->format('Y-m-d H:i:s'),
        );
    }

    public function test_grace_until_for_pause_exactly_at_21_msk_is_day_plus_two_21_msk(): void
    {
        $pausedAt = CarbonImmutable::parse('2026-05-25 21:00:00', 'Europe/Moscow');
        $graceUntil = $this->guard->computeGraceUntil($pausedAt);

        $this->assertSame(
            '2026-05-27 21:00:00',
            $graceUntil->setTimezone('Europe/Moscow')->format('Y-m-d H:i:s'),
        );
    }

    public function test_grace_until_handles_utc_input(): void
    {
        // 14:00 UTC = 17:00 MSK (до 21:00) → grace = следующее 21:00 МСК +24ч
        $pausedAt = CarbonImmutable::parse('2026-05-25 14:00:00', 'UTC');
        $graceUntil = $this->guard->computeGraceUntil($pausedAt);

        $this->assertSame(
            '2026-05-26 21:00:00',
            $graceUntil->setTimezone('Europe/Moscow')->format('Y-m-d H:i:s'),
        );
    }

    // ------------------ isProtected ---------------------------------------

    private function mockLinksExists(int $projectId, bool $exists): void
    {
        $builder = \Mockery::mock();
        $builder->shouldReceive('where')->with('project_id', $projectId)->andReturnSelf();
        $builder->shouldReceive('exists')->andReturn($exists);

        DB::shouldReceive('table')->with('project_supplier_links')->andReturn($builder);
    }

    public function test_is_protected_false_when_no_supplier_links(): void
    {
        $project = new Project(['is_active' => true]);
        $project->id = 1;

        $this->mockLinksExists(1, false);

        $this->assertFalse($this->guard->isProtected($project));
    }

    public function test_is_protected_true_when_active_and_linked(): void
    {
        $project = new Project(['is_active' => true]);
        $project->id = 2;

        $this->mockLinksExists(2, true);

        $this->assertTrue($this->guard->isProtected($project));
    }

    public function test_is_protected_false_when_paused_without_paused_at_legacy(): void
    {
        $project = new Project(['is_active' => false]);
        $project->id = 3;
        $project->paused_at = null;

        $this->mockLinksExists(3, true);

        $this->assertFalse($this->guard->isProtected($project));
    }

    public function test_is_protected_true_when_paused_recently_within_grace(): void
    {
        $project = new Project(['is_active' => false]);
        $project->id = 4;
        // paused at 22:00 МСК → grace until +day-after-tomorrow 21:00 МСК
        $project->paused_at = CarbonImmutable::parse('2026-05-25 22:00:00', 'Europe/Moscow');

        $this->mockLinksExists(4, true);

        // current "now" is one hour after pause — well inside grace window
        $now = CarbonImmutable::parse('2026-05-25 23:00:00', 'Europe/Moscow');
        $this->assertTrue($this->guard->isProtected($project, $now));
    }

    public function test_is_protected_false_when_grace_has_elapsed(): void
    {
        $project = new Project(['is_active' => false]);
        $project->id = 5;
        $project->paused_at = CarbonImmutable::parse('2026-05-25 14:00:00', 'Europe/Moscow');

        $this->mockLinksExists(5, true);

        // grace_until = 2026-05-26 21:00; "now" — позже
        $now = CarbonImmutable::parse('2026-05-26 22:00:00', 'Europe/Moscow');
        $this->assertFalse($this->guard->isProtected($project, $now));
    }

    // ------------------ assertCanMutateSource -----------------------------

    public function test_assert_no_throw_when_unprotected(): void
    {
        $project = new Project(['is_active' => true]);
        $project->id = 6;

        $this->mockLinksExists(6, false);

        // not throwing means success
        $this->guard->assertCanMutateSource($project, 'delete');
        $this->assertTrue(true);
    }

    public function test_assert_throws_422_with_delete_phrasing(): void
    {
        // Фиксируем время до 21:00 → хвост до завтра (26 июня); текст time-aware.
        CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-06-25 14:00', 'Europe/Moscow'));
        $project = new Project(['is_active' => true]);
        $project->id = 7;

        $this->mockLinksExists(7, true);

        try {
            $this->guard->assertCanMutateSource($project, 'delete');
            $this->fail('Expected HttpResponseException');
        } catch (HttpResponseException $e) {
            $this->assertSame(422, $e->getResponse()->getStatusCode());
            $body = json_decode((string) $e->getResponse()->getContent(), true);
            $msg = $body['errors']['project'][0];
            $this->assertStringContainsString('Мы уже начали сбор лидов', $msg);
            $this->assertStringContainsString('Удалить можно будет после 26 июня', $msg);
            // Больше НЕ должно быть захардкоженного «сегодня в 18:00» (врёт после 18:00).
            $this->assertStringNotContainsString('сегодня в 18:00', $msg);
        } finally {
            CarbonImmutable::setTestNow();
        }
    }

    public function test_assert_throws_422_with_change_source_phrasing(): void
    {
        CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-06-25 14:00', 'Europe/Moscow'));
        $project = new Project(['is_active' => true]);
        $project->id = 8;

        $this->mockLinksExists(8, true);
        $this->mockSnapshotFlag(false); // старый путь — смена источника заблокирована

        try {
            $this->guard->assertCanMutateSource($project, 'change_source');
            $this->fail('Expected HttpResponseException');
        } catch (HttpResponseException $e) {
            $msg = json_decode((string) $e->getResponse()->getContent(), true)['errors']['project'][0];
            $this->assertStringContainsString('Изменить источник можно будет после 26 июня', $msg);
        } finally {
            CarbonImmutable::setTestNow();
        }
    }

    public function test_assert_does_not_throw_for_change_source_when_snapshot_routing_enabled(): void
    {
        $project = new Project(['is_active' => true]);
        $project->id = 9;

        $this->mockLinksExists(9, true);
        $this->mockSnapshotFlag(true); // Путь A включён — смена источника разрешена

        // не бросает = успех (раздача доводит хвост по слепку)
        $this->guard->assertCanMutateSource($project, 'change_source');
        $this->assertTrue(true);
    }

    public function test_assert_still_throws_for_delete_even_when_snapshot_routing_enabled(): void
    {
        $project = new Project(['is_active' => true]);
        $project->id = 10;

        $this->mockLinksExists(10, true);
        // delete не проверяет флаг — защита delete остаётся

        $this->expectException(HttpResponseException::class);
        $this->guard->assertCanMutateSource($project, 'delete');
    }

    private function mockSnapshotFlag(bool $on): void
    {
        $row = $on ? (object) ['value' => 'true'] : null;
        $builder = \Mockery::mock();
        $builder->shouldReceive('where')->with('key', 'routing_match_by_snapshot')->andReturnSelf();
        $builder->shouldReceive('first')->andReturn($row);

        DB::shouldReceive('table')->with('system_settings')->andReturn($builder);
    }
}
