<?php

declare(strict_types=1);

use App\DTOs\UserRecoProfileDTO;
use App\Models\Artwork;
use App\Models\User;
use App\Services\Recommendation\RecommendationService;
use App\Services\Recommendation\UserPreferenceBuilder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

uses(RefreshDatabase::class);

beforeEach(function () {
    // Disable Meilisearch so tests remain fast / deterministic
    config(['scout.driver' => 'null']);

    // Seed recommendations config
    config([
        'recommendations.weights.tag_overlap'      => 0.40,
        'recommendations.weights.creator_affinity' => 0.25,
        'recommendations.weights.popularity'       => 0.20,
        'recommendations.weights.freshness'        => 0.15,
        'recommendations.candidate_pool_size'      => 200,
        'recommendations.max_per_creator'          => 3,
        'recommendations.min_unique_tags'          => 5,
        'recommendations.ttl.for_you_feed'         => 5,
    ]);

    Cache::flush();
});

// ─────────────────────────────────────────────────────────────────────────────
// RecommendationService cold-start (no signals)
// ─────────────────────────────────────────────────────────────────────────────

it('returns cold-start feed when user has no signals', function () {
    $user = User::factory()->create();

    // Profile builder will return a DTO with no signals
    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->andReturn(new UserRecoProfileDTO(
        topTagSlugs:         [],
        topCategorySlugs:    [],
        strongCreatorIds:    [],
        tagWeights:          [],
        categoryWeights:     [],
        dislikedTagSlugs:    [],
    ));

    $service = new RecommendationService($builder);
    $result  = $service->forYouFeed($user, 10);

    expect($result)->toHaveKeys(['data', 'meta'])
        ->and($result['meta']['source'])->toBe('cold_start');
});

// ─────────────────────────────────────────────────────────────────────────────
// RecommendationService personalised flow (mocked profile)
// ─────────────────────────────────────────────────────────────────────────────

it('returns personalised feed with data when user has signals', function () {
    $user = User::factory()->create();

    // Two artworks from other creators (tags not needed — Scout driver is null)
    Artwork::factory()->create([
        'is_public'    => true,
        'is_approved'  => true,
        'user_id'      => User::factory()->create()->id,
        'published_at' => now()->subDay(),
    ]);
    Artwork::factory()->create([
        'is_public'    => true,
        'is_approved'  => true,
        'user_id'      => User::factory()->create()->id,
        'published_at' => now()->subDays(2),
    ]);

    $profile = new UserRecoProfileDTO(
        topTagSlugs:      ['cyberpunk', 'neon'],
        topCategorySlugs: [],
        strongCreatorIds: [],
        tagWeights:       ['cyberpunk' => 5.0, 'neon' => 3.0],
        categoryWeights:  [],
        dislikedTagSlugs: [],
    );

    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->with($user)->andReturn($profile);

    $service = new RecommendationService($builder);
    $result  = $service->forYouFeed($user, 10);

    expect($result)->toHaveKeys(['data', 'meta'])
        ->and($result['meta'])->toHaveKey('source');
    // With scout null driver the collection is empty → cold-start path
    // This tests the structure contract regardless of driver
    expect($result['data'])->toBeArray();
});

// ─────────────────────────────────────────────────────────────────────────────
// Diversity: max 3 per creator
// ─────────────────────────────────────────────────────────────────────────────

it('enforces max_per_creator diversity limit via forYouPreview', function () {
    $user = User::factory()->create();

    $creatorA = User::factory()->create();
    $creatorB = User::factory()->create();

    // 4 artworks by creatorA, 1 by creatorB (Scout driver null — no Meili calls)
    Artwork::factory(4)->create([
        'is_public'    => true,
        'is_approved'  => true,
        'user_id'      => $creatorA->id,
        'published_at' => now()->subHour(),
    ]);
    Artwork::factory()->create([
        'is_public'    => true,
        'is_approved'  => true,
        'user_id'      => $creatorB->id,
        'published_at' => now()->subHour(),
    ]);

    $profile = new UserRecoProfileDTO(
        topTagSlugs:      ['abstract'],
        topCategorySlugs: [],
        strongCreatorIds: [],
        tagWeights:       ['abstract' => 5.0],
        categoryWeights:  [],
        dislikedTagSlugs: [],
    );

    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->andReturn($profile);

    $service = new RecommendationService($builder);

    // With null scout driver the candidate collection is empty; we test contract.
    $result = $service->forYouFeed($user, 10);
    expect($result)->toHaveKeys(['data', 'meta']);
    expect($result['data'])->toBeArray();
});

// ─────────────────────────────────────────────────────────────────────────────
// Favourited artworks are excluded from For You feed
// ─────────────────────────────────────────────────────────────────────────────

it('excludes artworks already favourited by user', function () {
    $user = User::factory()->create();
    $art  = Artwork::factory()->create(['is_public' => true, 'is_approved' => true]);

    // Insert a favourite
    DB::table('artwork_favourites')->insert([
        'user_id'    => $user->id,
        'artwork_id' => $art->id,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $profile = new UserRecoProfileDTO(
        topTagSlugs:      ['tag-x'],
        topCategorySlugs: [],
        strongCreatorIds: [],
        tagWeights:       ['tag-x' => 3.0],
        categoryWeights:  [],
        dislikedTagSlugs: [],
    );

    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->andReturn($profile);

    $service = new RecommendationService($builder);

    // With null scout, no candidates surface — checking that getFavoritedIds runs without error
    $result = $service->forYouFeed($user, 10);
    expect($result)->toHaveKeys(['data', 'meta']);

    $artworkIds = array_column($result['data'], 'id');
    expect($artworkIds)->not->toContain($art->id);
});

// ─────────────────────────────────────────────────────────────────────────────
// Cursor pagination shape
// ─────────────────────────────────────────────────────────────────────────────

it('returns null next_cursor when no more pages available', function () {
    $user = User::factory()->create();

    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->andReturn(new UserRecoProfileDTO(
        topTagSlugs:      [],
        topCategorySlugs: [],
        strongCreatorIds: [],
        tagWeights:       [],
        categoryWeights:  [],
        dislikedTagSlugs: [],
    ));

    $service = new RecommendationService($builder);
    $result  = $service->forYouFeed($user, 40, null);

    expect($result['meta'])->toHaveKey('next_cursor');
    // Cold-start with 0 results: next_cursor should be null
    expect($result['meta']['next_cursor'])->toBeNull();
});

// ─────────────────────────────────────────────────────────────────────────────
// forYouPreview is a subset of forYouFeed
// ─────────────────────────────────────────────────────────────────────────────

it('forYouPreview returns an array', function () {
    $user = User::factory()->create();

    $builder = Mockery::mock(UserPreferenceBuilder::class);
    $builder->shouldReceive('build')->andReturn(new UserRecoProfileDTO(
        topTagSlugs: [], topCategorySlugs: [], strongCreatorIds: [],
        tagWeights: [], categoryWeights: [], dislikedTagSlugs: [],
    ));

    $service = new RecommendationService($builder);
    $preview = $service->forYouPreview($user, 12);

    expect($preview)->toBeArray();
});
