<?php

use App\Services\ChurchToolsService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

beforeEach(function () {
    ensureSyncTables();
});

test('cts:sync synchronisiert services, agenda songs und schreibt sync log', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    $localSongId = DB::table('songs')->insertGetId([
        'title' => 'Way Maker',
        'ccli_id' => '7115744',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(
                id: 100,
                title: 'Gottesdienst Sonntag',
                startDate: '2026-03-08T10:00:00+00:00',
                note: 'Probe',
                eventServices: [
                    new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')),
                    new FakeEventService('Beamer', new FakePerson('Lisa', 'Technik')),
                ],
            ),
        ],
        songFetcher: fn () => [new FakeSong(id: 1, title: 'Way Maker', ccli: '7115744')],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 1, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 2, position: '2', title: 'Way Maker', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
            new FakeAgendaItem(id: 3, position: '3', title: 'Unbekannt', type: 'Song', song: new FakeSong(id: 5002, title: 'Unbekannt', ccli: '9999999')),
        ]),
        eventServiceFetcher: fn (int $eventId) => [
            new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')),
            new FakeEventService('Beamer', new FakePerson('Lisa', 'Technik')),
        ],
    ));

    Artisan::call('cts:sync');

    expect(Artisan::output())->toContain('Daten wurden aktualisiert');

    $service = DB::table('services')->where('cts_event_id', '100')->first();
    expect($service)->not->toBeNull();
    expect($service->title)->toBe('Gottesdienst Sonntag');
    expect($service->preacher_name)->toBe('Max Mustermann');
    expect($service->beamer_tech_name)->toBe('Lisa Technik');

    $matchedSong = DB::table('service_songs')->where('order', 2)->first();
    expect($matchedSong)->not->toBeNull();
    expect((int) $matchedSong->song_id)->toBe($localSongId);
    expect($matchedSong->matched_at)->not->toBeNull();

    $unmatchedSong = DB::table('service_songs')->where('order', 3)->first();
    expect($unmatchedSong)->not->toBeNull();
    expect($unmatchedSong->song_id)->toBeNull();
    expect($unmatchedSong->cts_ccli_id)->toBe('9999999');

    $syncLog = DB::table('cts_sync_log')->latest('id')->first();
    expect($syncLog)->not->toBeNull();
    expect($syncLog->status)->toBe('success');
    expect((int) $syncLog->events_count)->toBe(1);
    expect((int) $syncLog->songs_count)->toBe(2);
});

test('sync speichert alle agenda items (songs und nicht-songs) in service_agenda_items', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 200, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 10, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 11, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
            new FakeAgendaItem(id: 12, position: '3', title: 'Predigt', type: 'Default', note: 'Zum Thema Liebe'),
            new FakeAgendaItem(id: 13, position: '4', title: 'Abschluss', type: 'Song', song: new FakeSong(id: 5002, title: 'Amazing Grace', ccli: '1234567')),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '200')->first();

    $agendaItems = DB::table('service_agenda_items')
        ->where('service_id', $service->id)
        ->orderBy('sort_order')
        ->get();

    expect($agendaItems)->toHaveCount(4);
    expect($agendaItems[0]->title)->toBe('Begrüssung');
    expect($agendaItems[0]->type)->toBe('Default');
    expect((int) $agendaItems[0]->sort_order)->toBe(1);
    expect($agendaItems[1]->title)->toBe('Lobpreis');
    expect($agendaItems[1]->type)->toBe('Song');
    expect((int) $agendaItems[1]->sort_order)->toBe(2);
    expect($agendaItems[2]->title)->toBe('Predigt');
    expect($agendaItems[2]->note)->toBe('Zum Thema Liebe');
    expect($agendaItems[3]->title)->toBe('Abschluss');
});

test('song items erstellen service_song UND service_agenda_item mit korrekter service_song_id', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 300, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 20, position: '1', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '300')->first();

    $serviceSong = DB::table('service_songs')->where('service_id', $service->id)->first();
    expect($serviceSong)->not->toBeNull();
    expect($serviceSong->cts_song_name)->toBe('Way Maker');

    $agendaItem = DB::table('service_agenda_items')->where('service_id', $service->id)->first();
    expect($agendaItem)->not->toBeNull();
    expect($agendaItem->title)->toBe('Lobpreis');
    expect((int) $agendaItem->service_song_id)->toBe((int) $serviceSong->id);
});

test('nicht-song items erstellen keine service_song eintraege', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 400, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 30, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 31, position: '2', title: 'Predigt', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '400')->first();

    $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get();
    expect($agendaItems)->toHaveCount(2);

    $serviceSongs = DB::table('service_songs')->where('service_id', $service->id)->get();
    expect($serviceSongs)->toHaveCount(0);
});

test('is_before_event items werden uebersprungen', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 500, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 40, position: '0', title: 'Soundcheck', type: 'Default', isBeforeEvent: true),
            new FakeAgendaItem(id: 41, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 42, position: '2', title: 'Probe', type: 'Default', isBeforeEvent: true),
            new FakeAgendaItem(id: 43, position: '3', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '500')->first();

    $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get();
    expect($agendaItems)->toHaveCount(2);

    $titles = $agendaItems->pluck('title')->toArray();
    expect($titles)->not->toContain('Soundcheck');
    expect($titles)->not->toContain('Probe');
    expect($titles)->toContain('Begrüssung');
    expect($titles)->toContain('Lobpreis');
});

test('verwaiste agenda items werden bei re-sync entfernt', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    // First sync with 3 items
    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 600, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 50, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 51, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
            new FakeAgendaItem(id: 52, position: '3', title: 'Predigt', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '600')->first();
    expect(DB::table('service_agenda_items')->where('service_id', $service->id)->count())->toBe(3);

    // Re-sync with only 1 item
    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 600, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 50, position: '1', title: 'Begrüssung', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get();
    expect($agendaItems)->toHaveCount(1);
    expect($agendaItems[0]->title)->toBe('Begrüssung');
});

test('slides werden erhalten (FK genullt) wenn verwaiste agenda items entfernt werden', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    // First sync with 2 items
    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 700, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 60, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 61, position: '2', title: 'Predigt', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '700')->first();
    $agendaItem = DB::table('service_agenda_items')
        ->where('service_id', $service->id)
        ->where('sort_order', 2)
        ->first();

    // Create a slide linked to the second agenda item
    $slideId = DB::table('slides')->insertGetId([
        'service_id' => $service->id,
        'service_agenda_item_id' => $agendaItem->id,
        'type' => 'sermon',
        'original_filename' => 'predigt.jpg',
        'stored_filename' => 'slides/predigt_stored.jpg',
        'thumbnail_filename' => 'predigt_thumb.jpg',
        'uploaded_at' => now(),
        'sort_order' => 1,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    // Re-sync with only 1 item (second item removed)
    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 700, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 60, position: '1', title: 'Begrüssung', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    // Slide still exists but FK is null
    $slide = DB::table('slides')->where('id', $slideId)->first();
    expect($slide)->not->toBeNull();
    expect($slide->service_agenda_item_id)->toBeNull();

    // Orphaned agenda item is gone
    expect(DB::table('service_agenda_items')->where('service_id', $service->id)->count())->toBe(1);
});

test('sync summary enthaelt agenda_items_count', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    $service = new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 800, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 70, position: '1', title: 'Begrüssung', type: 'Default'),
            new FakeAgendaItem(id: 71, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    );

    $summary = $service->sync();

    expect($summary)->toHaveKey('agenda_items_count');
    expect($summary['agenda_items_count'])->toBe(2);
    expect($summary['songs_count'])->toBe(1);
    expect($summary['services_count'])->toBe(1);
});

test('event ohne agenda setzt has_agenda auf false und ueberspringt agenda sync', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 893, title: 'Service ohne Agenda', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn (int $eventId) => throw new CTRequestException(
            "Agenda for event [{$eventId}] not found."
        ),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '893')->first();
    expect($service)->not->toBeNull();
    expect($service->title)->toBe('Service ohne Agenda');
    expect((bool) $service->has_agenda)->toBeFalse();

    $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get();
    expect($agendaItems)->toHaveCount(0);

    $serviceSongs = DB::table('service_songs')->where('service_id', $service->id)->get();
    expect($serviceSongs)->toHaveCount(0);

    $syncLog = DB::table('cts_sync_log')->latest('id')->first();
    expect($syncLog)->not->toBeNull();
    expect($syncLog->status)->toBe('success');
});

test('event mit agenda setzt has_agenda auf true', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 894, title: 'Service mit Agenda', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(id: 90, position: '1', title: 'Begrüssung', type: 'Default'),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '894')->first();
    expect($service)->not->toBeNull();
    expect((bool) $service->has_agenda)->toBeTrue();

    $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get();
    expect($agendaItems)->toHaveCount(1);
});

test('responsible feld wird als json gespeichert', function () {
    Carbon::setTestNow('2026-03-01 09:00:00');

    app()->instance(ChurchToolsService::class, new ChurchToolsService(
        eventFetcher: fn () => [
            new FakeEvent(id: 900, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'),
        ],
        songFetcher: fn () => [],
        agendaFetcher: fn () => new FakeAgenda([
            new FakeAgendaItem(
                id: 80,
                position: '1',
                title: 'Begrüssung',
                type: 'Default',
                responsible: ['name' => 'Max Mustermann'],
            ),
        ]),
        eventServiceFetcher: fn (int $eventId) => [],
    ));

    Artisan::call('cts:sync');

    $service = DB::table('services')->where('cts_event_id', '900')->first();
    $agendaItem = DB::table('service_agenda_items')->where('service_id', $service->id)->first();

    $responsible = json_decode($agendaItem->responsible, true);
    expect($responsible)->toBe(['name' => 'Max Mustermann']);
});

function ensureSyncTables(): void
{
    if (! Schema::hasTable('services')) {
        Schema::create('services', function (Blueprint $table) {
            $table->id();
            $table->string('cts_event_id')->unique();
            $table->string('title');
            $table->date('date')->nullable();
            $table->string('preacher_name')->nullable();
            $table->string('beamer_tech_name')->nullable();
            $table->timestamp('last_synced_at')->nullable();
            $table->json('cts_data')->nullable();
            $table->boolean('has_agenda')->default(false);
            $table->timestamps();
        });
    }

    if (! Schema::hasTable('songs')) {
        Schema::create('songs', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('ccli_id')->nullable()->unique();
            $table->string('cts_song_id')->nullable()->index();
            $table->timestamps();
        });
    }

    if (! Schema::hasTable('service_songs')) {
        Schema::create('service_songs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
            $table->foreignId('song_id')->nullable()->constrained('songs')->nullOnDelete();
            $table->string('cts_song_name');
            $table->string('cts_ccli_id')->nullable();
            $table->string('cts_song_id')->nullable();
            $table->unsignedInteger('order')->default(0);
            $table->timestamp('matched_at')->nullable();
            $table->timestamps();
            $table->unique(['service_id', 'order']);
        });
    }

    if (! Schema::hasTable('service_agenda_items')) {
        Schema::create('service_agenda_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
            $table->string('cts_agenda_item_id')->nullable()->index();
            $table->string('position');
            $table->string('title');
            $table->string('type');
            $table->text('note')->nullable();
            $table->string('duration')->nullable();
            $table->string('start')->nullable();
            $table->boolean('is_before_event')->default(false);
            $table->json('responsible')->nullable();
            $table->foreignId('service_song_id')->nullable()->constrained('service_songs')->nullOnDelete();
            $table->unsignedInteger('sort_order');
            $table->timestamps();
            $table->unique(['service_id', 'sort_order']);
        });
    }

    if (! Schema::hasTable('slides')) {
        Schema::create('slides', function (Blueprint $table) {
            $table->id();
            $table->enum('type', ['information', 'moderation', 'sermon']);
            $table->foreignId('service_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('service_agenda_item_id')->nullable()->constrained('service_agenda_items')->nullOnDelete();
            $table->string('original_filename');
            $table->string('stored_filename');
            $table->string('thumbnail_filename');
            $table->date('expire_date')->nullable();
            $table->string('uploader_name')->nullable();
            $table->timestamp('uploaded_at');
            $table->unsignedInteger('sort_order')->default(0);
            $table->softDeletes();
            $table->timestamps();
        });
    }

    if (! Schema::hasTable('cts_sync_log')) {
        Schema::create('cts_sync_log', function (Blueprint $table) {
            $table->id();
            $table->timestamp('synced_at')->nullable();
            $table->unsignedInteger('events_count')->default(0);
            $table->unsignedInteger('songs_count')->default(0);
            $table->string('status');
            $table->text('error')->nullable();
            $table->timestamps();
        });
    }
}

final class FakeEvent
{
    public function __construct(
        private readonly int $id,
        private readonly string $title,
        private readonly string $startDate,
        private readonly ?string $note = null,
        private readonly array $eventServices = [],
    ) {}

    public function getId(): string
    {
        return (string) $this->id;
    }

    public function getName(): string
    {
        return $this->title;
    }

    public function getStartDate(): string
    {
        return $this->startDate;
    }

    public function getNote(): ?string
    {
        return $this->note;
    }

    public function getEventServices(): array
    {
        return $this->eventServices;
    }
}

final class FakeEventService
{
    public function __construct(
        private readonly string $name,
        private readonly ?FakePerson $person,
    ) {}

    public function getName(): string
    {
        return $this->name;
    }

    public function getPerson(): ?FakePerson
    {
        return $this->person;
    }
}

final class FakePerson
{
    public function __construct(
        private readonly string $firstName,
        private readonly string $lastName,
    ) {}

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }
}

final class FakeAgenda
{
    public function __construct(private readonly array $items) {}

    public function getItems(): array
    {
        return $this->items;
    }

    public function getSongs(): array
    {
        return array_values(array_filter(
            array_map(fn ($item) => $item->getSong(), $this->items),
            fn ($song) => $song !== null,
        ));
    }
}

final class FakeAgendaItem
{
    public function __construct(
        private readonly int $id,
        private readonly string $position,
        private readonly string $title,
        private readonly string $type,
        private readonly ?FakeSong $song = null,
        private readonly ?string $note = null,
        private readonly ?string $duration = null,
        private readonly ?string $start = null,
        private readonly bool $isBeforeEvent = false,
        private readonly ?array $responsible = null,
    ) {}

    public function getId(): string
    {
        return (string) $this->id;
    }

    public function getPosition(): string
    {
        return $this->position;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getSong(): ?FakeSong
    {
        return $this->song;
    }

    public function getNote(): ?string
    {
        return $this->note;
    }

    public function getDuration(): ?string
    {
        return $this->duration;
    }

    public function getStart(): ?string
    {
        return $this->start;
    }

    public function getIsBeforeEvent(): bool
    {
        return $this->isBeforeEvent;
    }

    public function getResponsible(): ?array
    {
        return $this->responsible;
    }
}

final class FakeSong
{
    public function __construct(
        private readonly int $id,
        private readonly string $title,
        private readonly ?string $ccli,
    ) {}

    public function getId(): string
    {
        return (string) $this->id;
    }

    public function getName(): string
    {
        return $this->title;
    }

    public function getCcli(): ?string
    {
        return $this->ccli;
    }
}
