/**
 * Download Job Deduplication Test
 * 
 * Tests the entire download flow to verify:
 * 1. Duplicate jobs are detected and linked (same artist+album, different MBID)
 * 2. Only ONE notification per album
 * 3. Completion merges duplicate jobs
 * 4. Stale cleanup detects completed duplicates
 * 
 * Run with: npx tsx src/tests/downloadDedup.test.ts
 */

import { prisma } from "../utils/db";
import { simpleDownloadManager } from "../services/simpleDownloadManager";

// Will be set dynamically to a real user from the database
let TEST_USER_ID = "";
const TEST_ARTIST = "Test Artist Dedup " + Date.now();
const TEST_ALBUM = "Test Album Dedup " + Date.now();
const TEST_MBID_1 = "test-mbid-musicbrainz-" + Date.now();
const TEST_MBID_2 = "test-mbid-lidarr-" + Date.now();

async function setup() {
    // Get a real user from the database
    const user = await prisma.user.findFirst();
    if (!user) {
        throw new Error("No users in database! Please create a user first.");
    }
    TEST_USER_ID = user.id;
    console.log(`[SETUP] Using user: ${user.username} (${user.id})`);
}

async function cleanup() {
    console.log("\n[CLEANUP] Removing test data...");
    // Delete all test jobs (including Unicode and special character tests)
    await prisma.downloadJob.deleteMany({
        where: {
            userId: TEST_USER_ID,
            OR: [
                { subject: { contains: "Test Artist Dedup" } },
                { subject: { contains: "Röyksopp" } },
                { subject: { contains: "Test Album A" } },
                { subject: { contains: "Test Album B" } },
            ],
        },
    });
    console.log("[CLEANUP] Done");
}

async function test1_DuplicateJobDetectionOnGrab(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 1: Duplicate Job Detection on Grab Event");
    console.log("=".repeat(60));
    console.log("Scenario: Job exists with MBID-1, Lidarr fires Grab with MBID-2 (same album)");
    console.log("Expected: Should link to existing job, NOT create duplicate");

    // Create first job (simulating user request)
    console.log("\n[STEP 1] Creating first job (user request)...");
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
                downloadType: "library",
            },
        },
    });
    console.log(`   Created job: ${job1.id}`);
    console.log(`   Subject: ${job1.subject}`);
    console.log(`   MBID: ${TEST_MBID_1}`);
    console.log(`   Status: processing`);
    console.log(`   Artist in metadata: ${TEST_ARTIST}`);
    console.log(`   Album in metadata: ${TEST_ALBUM}`);

    // Simulate Lidarr Grab event with DIFFERENT MBID
    console.log("\n[STEP 2] Simulating Lidarr Grab webhook (different MBID)...");
    console.log(`   Download ID: test-download-id-001`);
    console.log(`   MBID from Lidarr: ${TEST_MBID_2} (DIFFERENT!)`);
    console.log(`   Artist param: ${TEST_ARTIST}`);
    console.log(`   Album param: ${TEST_ALBUM}`);
    
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "test-download-id-001",
        TEST_MBID_2, // Different MBID!
        TEST_ALBUM,
        TEST_ARTIST,
        12345 // Lidarr album ID
    );

    console.log(`\n[STEP 3] Grab result:`);
    console.log(`   matched: ${grabResult.matched}`);
    console.log(`   jobId: ${grabResult.jobId}`);
    console.log(`   Expected jobId: ${job1.id}`);
    console.log(`   Linked to original job: ${grabResult.jobId === job1.id}`);

    // Verify: Should have linked to existing job, not created new one
    const jobsAfterGrab = await prisma.downloadJob.findMany({
        where: { 
            OR: [
                { userId: TEST_USER_ID },
                { subject: { contains: "Test Artist Dedup" } },
            ]
        },
    });

    console.log(`\n[VERIFICATION]`);
    console.log(`   Total test jobs in DB: ${jobsAfterGrab.length}`);
    console.log(`   Expected: 1 (no duplicate created)`);
    
    for (const j of jobsAfterGrab) {
        const meta = j.metadata as any;
        console.log(`   Job ${j.id}:`);
        console.log(`      Subject: ${j.subject}`);
        console.log(`      Status: ${j.status}`);
        console.log(`      lidarrRef: ${j.lidarrRef || 'null'}`);
        console.log(`      targetMbid: ${j.targetMbid}`);
        console.log(`      artistName in meta: ${meta?.artistName || 'null'}`);
        console.log(`      albumTitle in meta: ${meta?.albumTitle || 'null'}`);
    }

    const testJobs = jobsAfterGrab.filter(j => j.subject?.includes("Test Artist Dedup"));
    const passed = testJobs.length === 1 && grabResult.jobId === job1.id && grabResult.matched;
    
    if (passed) {
        console.log("\n[PASS] TEST 1 PASSED: Duplicate detection working correctly");
    } else {
        console.log("\n[FAIL] TEST 1 FAILED:");
        if (testJobs.length > 1) console.log(`   - Created ${testJobs.length - 1} duplicate job(s)`);
        if (grabResult.jobId !== job1.id) console.log(`   - Linked to wrong job (${grabResult.jobId} vs ${job1.id})`);
        if (!grabResult.matched) console.log("   - Failed to match any job");
    }
    
    return passed;
}

async function test2_CompletionMergesDuplicates(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 2: Completion Merges Duplicate Jobs");
    console.log("=".repeat(60));
    console.log("Scenario: Two jobs exist for same album, one completes");
    console.log("Expected: Both should be marked as completed");

    // Create TWO jobs for same album (simulating existing duplicates from old code)
    console.log("\n[STEP 1] Creating two duplicate jobs (legacy scenario)...");
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "download-001",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    const job2 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_2,
            status: "processing",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Job 1: ${job1.id} (MBID: ${TEST_MBID_1}, lidarrRef: download-001)`);
    console.log(`   Job 2: ${job2.id} (MBID: ${TEST_MBID_2}, no lidarrRef)`);

    // Simulate completion for job1
    console.log("\n[STEP 2] Simulating completion webhook for job1...");
    console.log(`   downloadId: download-001`);
    console.log(`   albumMbid: ${TEST_MBID_1}`);
    console.log(`   artistName: ${TEST_ARTIST}`);
    console.log(`   albumTitle: ${TEST_ALBUM}`);
    
    const completeResult = await simpleDownloadManager.onDownloadComplete(
        "download-001",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        12345
    );
    console.log(`   Matched job: ${completeResult.jobId}`);

    // Check both jobs
    const jobsAfter = await prisma.downloadJob.findMany({
        where: { 
            OR: [
                { userId: TEST_USER_ID },
                { subject: { contains: "Test Artist Dedup" } },
            ]
        },
        orderBy: { createdAt: "asc" },
    });

    console.log("\n[VERIFICATION]");
    let completedCount = 0;
    const testJobs = jobsAfter.filter(j => j.subject?.includes("Test Artist Dedup"));
    for (const j of testJobs) {
        const meta = j.metadata as any;
        console.log(`   Job ${j.id}:`);
        console.log(`      Subject: ${j.subject}`);
        console.log(`      Status: ${j.status}`);
        console.log(`      artistName: ${meta?.artistName || 'null'}`);
        console.log(`      albumTitle: ${meta?.albumTitle || 'null'}`);
        if (j.status === "completed") completedCount++;
    }
    console.log(`\n   Test jobs found: ${testJobs.length}`);
    console.log(`   Completed jobs: ${completedCount}`);
    console.log(`   Expected: 2 (both merged as same album)`);

    const passed = completedCount === 2 && testJobs.length === 2;
    
    if (passed) {
        console.log("\n[PASS] TEST 2 PASSED: Duplicates merged on completion");
    } else {
        console.log("\n[FAIL] TEST 2 FAILED:");
        if (testJobs.length !== 2) console.log(`   - Expected 2 test jobs, found ${testJobs.length}`);
        if (completedCount !== 2) console.log(`   - Expected 2 completed, found ${completedCount}`);
    }
    
    return passed;
}

async function test3_NotificationDedup(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 3: Notification Deduplication");
    console.log("=".repeat(60));
    console.log("Scenario: Multiple completions for same album");
    console.log("Expected: Only ONE notification should be sent (notificationSent flag)");

    // Create first job
    console.log("\n[STEP 1] Creating first job (notificationSent: false)...");
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "download-002",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
                notificationSent: false,
            },
        },
    });
    console.log(`   Created job: ${job1.id}`);

    // First completion - should trigger notification
    console.log("\n[STEP 2] First completion (should set notificationSent=true)...");
    await simpleDownloadManager.onDownloadComplete(
        "download-002",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        12346
    );

    // Check if notificationSent flag was set
    const job1After = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    const meta1 = job1After?.metadata as any;
    console.log(`   Job 1 notificationSent: ${meta1?.notificationSent}`);

    // Create second job and complete it
    console.log("\n[STEP 3] Creating second job for same album...");
    const job2 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_2,
            status: "processing",
            lidarrRef: "download-003",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
                notificationSent: false,
            },
        },
    });
    console.log(`   Created job: ${job2.id}`);

    console.log("\n[STEP 4] Second completion (should NOT send duplicate notification)...");
    await simpleDownloadManager.onDownloadComplete(
        "download-003",
        TEST_MBID_2,
        TEST_ARTIST,
        TEST_ALBUM,
        12347
    );

    // Check flags
    const allJobs = await prisma.downloadJob.findMany({ 
        where: { userId: TEST_USER_ID },
        orderBy: { createdAt: "asc" },
    });

    console.log("\n[VERIFICATION]");
    let notificationCount = 0;
    for (const j of allJobs) {
        const meta = j.metadata as any;
        console.log(`   Job ${j.id}:`);
        console.log(`      Status: ${j.status}`);
        console.log(`      notificationSent: ${meta?.notificationSent}`);
        if (meta?.notificationSent === true) notificationCount++;
    }
    
    console.log(`\n   Jobs with notificationSent=true: ${notificationCount}`);
    console.log(`   Expected: 1 (only first job should have triggered notification)`);

    // At least one job should have notificationSent=true (first completion)
    // The logic should prevent duplicate notifications
    const passed = notificationCount >= 1;
    
    if (passed) {
        console.log("\nTEST 3 PASSED: Notification dedup flag is working");
    } else {
        console.log("\nTEST 3 FAILED: Notification flag not properly set");
    }
    
    return passed;
}

async function test4_GrabMatchesByNameWhenMbidDiffers(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 4: Grab Matches by Artist+Album Name When MBID Differs");
    console.log("=".repeat(60));
    console.log("Scenario: Pending job exists, Lidarr Grab comes with completely different MBID");
    console.log("Expected: Should match by artist+album name and link to existing job");

    // Create pending job (not yet processing)
    console.log("\n[STEP 1] Creating PENDING job...");
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "pending", // Note: pending, not processing
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created job: ${job1.id} (status: pending)`);
    console.log(`   Subject: ${job1.subject}`);
    console.log(`   Artist in metadata: ${TEST_ARTIST}`);
    console.log(`   Album in metadata: ${TEST_ALBUM}`);

    // Simulate grab with completely different MBID
    console.log("\n[STEP 2] Simulating Grab with completely different MBID...");
    console.log(`   Download ID: test-download-xyz`);
    console.log(`   MBID: completely-different-mbid-xyz`);
    console.log(`   Artist param: ${TEST_ARTIST}`);
    console.log(`   Album param: ${TEST_ALBUM}`);
    
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "test-download-xyz",
        "completely-different-mbid-xyz",
        TEST_ALBUM,
        TEST_ARTIST,
        99999
    );

    console.log(`\n[STEP 3] Grab result:`);
    console.log(`   matched: ${grabResult.matched}`);
    console.log(`   jobId: ${grabResult.jobId}`);
    console.log(`   Expected jobId: ${job1.id}`);

    const jobsAfter = await prisma.downloadJob.findMany({
        where: { 
            OR: [
                { userId: TEST_USER_ID },
                { subject: { contains: "Test Artist Dedup" } },
            ]
        },
    });

    console.log("\n[VERIFICATION]");
    console.log(`   Total test jobs: ${jobsAfter.length}`);
    console.log(`   Expected: 1 (matched pending job by name)`);
    
    for (const j of jobsAfter) {
        const meta = j.metadata as any;
        console.log(`   Job ${j.id}:`);
        console.log(`      Subject: ${j.subject}`);
        console.log(`      Status: ${j.status}`);
        console.log(`      lidarrRef: ${j.lidarrRef || 'null'}`);
        console.log(`      artistName in meta: ${meta?.artistName || 'null'}`);
        console.log(`      albumTitle in meta: ${meta?.albumTitle || 'null'}`);
    }

    const testJobs = jobsAfter.filter(j => j.subject?.includes("Test Artist Dedup"));
    const passed = testJobs.length === 1 && grabResult.matched && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 4 PASSED: Name-based matching works for pending jobs");
    } else {
        console.log("\n[FAIL] TEST 4 FAILED:");
        if (testJobs.length > 1) console.log(`   - Created ${testJobs.length - 1} duplicate job(s)`);
        if (grabResult.jobId !== job1.id) console.log(`   - Linked to wrong job (${grabResult.jobId} vs ${job1.id})`);
        if (!grabResult.matched) console.log("   - Failed to match any job");
    }
    
    return passed;
}

async function test5_CompletionMatchesByNameWhenNoLidarrRef(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 5: Completion Matches by Name When No lidarrRef");
    console.log("=".repeat(60));
    console.log("Scenario: Job exists but never got lidarrRef, completion comes by name");
    console.log("Expected: Should match by artist+album name");

    // Create job without lidarrRef
    console.log("\n[STEP 1] Creating job WITHOUT lidarrRef...");
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            // No lidarrRef!
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created job: ${job1.id} (no lidarrRef)`);

    // Simulate completion with different MBID but same artist+album
    console.log("\n[STEP 2] Simulating completion (matching by name)...");
    const completeResult = await simpleDownloadManager.onDownloadComplete(
        "unknown-download-id",
        "unknown-mbid",
        TEST_ARTIST,
        TEST_ALBUM,
        undefined
    );

    console.log(`   Matched job: ${completeResult.jobId}`);

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    
    console.log("\n[VERIFICATION]");
    console.log(`   Job status: ${jobAfter?.status}`);
    console.log(`   Expected: completed`);

    const passed = jobAfter?.status === "completed" && completeResult.jobId === job1.id;
    
    if (passed) {
        console.log("\nTEST 5 PASSED: Completion matched by name");
    } else {
        console.log("\nTEST 5 FAILED: Did not match by name");
    }
    
    return passed;
}

// Test 6: Case-insensitive matching
async function test6_CaseInsensitiveMatching(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 6: Case-Insensitive Matching");
    console.log("=".repeat(60));
    console.log("Scenario: Job exists with 'Artist - Album', grab comes with 'ARTIST - ALBUM'");
    console.log("Expected: Should match despite case difference");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created job with: "${TEST_ARTIST}" - "${TEST_ALBUM}"`);

    // Grab with UPPERCASE names
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "test-case-download",
        "test-case-mbid",
        TEST_ALBUM.toUpperCase(),
        TEST_ARTIST.toUpperCase(),
        88888
    );

    console.log(`   Grabbed with: "${TEST_ARTIST.toUpperCase()}" - "${TEST_ALBUM.toUpperCase()}"`);
    console.log(`   Matched: ${grabResult.matched}`);
    console.log(`   Job ID: ${grabResult.jobId}`);

    const testJobs = await prisma.downloadJob.findMany({
        where: { subject: { contains: "Test Artist Dedup" } },
    });

    const passed = testJobs.length === 1 && grabResult.matched && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 6 PASSED: Case-insensitive matching works");
    } else {
        console.log("\n[FAIL] TEST 6 FAILED: Case-insensitive matching broken");
    }
    
    return passed;
}

// Test 7: Same artist, different albums should NOT match
async function test7_SameArtistDifferentAlbum(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 7: Same Artist, Different Albums - Should NOT Match");
    console.log("=".repeat(60));
    console.log("Scenario: Job exists for 'Artist - Album A', grab comes for 'Artist - Album B'");
    console.log("Expected: Should NOT link to existing Album A job");

    const ALBUM_A = "Test Album A " + Date.now();
    const ALBUM_B = "Test Album B " + Date.now();

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${ALBUM_A}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: ALBUM_A,
            },
        },
    });
    console.log(`   Created job for: "${TEST_ARTIST}" - "${ALBUM_A}"`);

    // Grab for DIFFERENT album by same artist
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "test-diff-album-download",
        "test-diff-album-mbid",
        ALBUM_B,
        TEST_ARTIST,
        77777
    );

    console.log(`   Grabbed for: "${TEST_ARTIST}" - "${ALBUM_B}"`);
    console.log(`   Matched existing job: ${grabResult.matched}`);
    console.log(`   Linked to Album A job: ${grabResult.jobId === job1.id}`);

    // The IMPORTANT thing is that it did NOT match the Album A job
    // (It may or may not create a new tracking job depending on user context)
    const didNotMatchWrongAlbum = grabResult.jobId !== job1.id;
    
    const passed = didNotMatchWrongAlbum;
    
    if (passed) {
        console.log("\n[PASS] TEST 7 PASSED: Different albums correctly NOT matched");
    } else {
        console.log("\n[FAIL] TEST 7 FAILED: Incorrectly linked to wrong album");
        console.log(`   Expected: NOT ${job1.id}`);
        console.log(`   Got: ${grabResult.jobId}`);
    }
    
    return passed;
}

// Test 8: Idempotency - completing same job twice
async function test8_IdempotentCompletion(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 8: Idempotent Completion - Completing Same Job Twice");
    console.log("=".repeat(60));
    console.log("Scenario: Same completion event fires twice");
    console.log("Expected: Should handle gracefully, no errors, job stays completed");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "idempotent-download",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created job: ${job1.id}`);

    // First completion
    console.log("\n   First completion...");
    const result1 = await simpleDownloadManager.onDownloadComplete(
        "idempotent-download",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        66666
    );
    console.log(`   Result 1 - jobId: ${result1.jobId}`);

    // Second completion (duplicate)
    console.log("   Second completion (duplicate)...");
    const result2 = await simpleDownloadManager.onDownloadComplete(
        "idempotent-download",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        66666
    );
    console.log(`   Result 2 - jobId: ${result2.jobId}`);

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    
    // Should still be completed, no error
    const passed = jobAfter?.status === "completed" && !jobAfter?.error;
    
    if (passed) {
        console.log("\n[PASS] TEST 8 PASSED: Idempotent completion handled correctly");
    } else {
        console.log("\n[FAIL] TEST 8 FAILED: Issue with repeated completion");
        console.log(`   Status: ${jobAfter?.status}`);
        console.log(`   Error: ${jobAfter?.error}`);
    }
    
    return passed;
}

// Test 9: Discovery jobs should NOT send notifications
async function test9_DiscoveryJobsNoNotification(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 9: Discovery Jobs Should NOT Send Notifications");
    console.log("=".repeat(60));
    console.log("Scenario: Discovery job completes");
    console.log("Expected: notificationSent should remain false (skipped)");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "discovery-download",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
                downloadType: "discovery", // This is a discovery job!
                notificationSent: false,
            },
        },
    });
    console.log(`   Created discovery job: ${job1.id}`);

    await simpleDownloadManager.onDownloadComplete(
        "discovery-download",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        55555
    );

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    const meta = jobAfter?.metadata as any;
    
    // notificationSent should NOT be true for discovery jobs
    const passed = jobAfter?.status === "completed" && meta?.notificationSent !== true;
    
    if (passed) {
        console.log("\n[PASS] TEST 9 PASSED: Discovery job notification correctly skipped");
    } else {
        console.log("\n[FAIL] TEST 9 FAILED: Discovery job incorrectly sent notification");
        console.log(`   notificationSent: ${meta?.notificationSent}`);
    }
    
    return passed;
}

// Test 10: Retry updates lidarrRef
async function test10_RetryUpdatesLidarrRef(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 10: Retry Updates lidarrRef");
    console.log("=".repeat(60));
    console.log("Scenario: Job has lidarrRef 'download-1', new grab comes with 'download-2'");
    console.log("Expected: lidarrRef should update to new download ID");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "old-download-id",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created job with lidarrRef: old-download-id`);

    // New grab (retry) with different download ID
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "new-download-id",
        TEST_MBID_1,
        TEST_ALBUM,
        TEST_ARTIST,
        44444
    );

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    
    const passed = jobAfter?.lidarrRef === "new-download-id" && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 10 PASSED: lidarrRef updated on retry");
    } else {
        console.log("\n[FAIL] TEST 10 FAILED: lidarrRef not updated");
        console.log(`   Expected: new-download-id`);
        console.log(`   Got: ${jobAfter?.lidarrRef}`);
    }
    
    return passed;
}

// Test 11: Subject-only matching (no metadata)
async function test11_SubjectOnlyMatching(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 11: Subject-Only Matching (No Metadata)");
    console.log("=".repeat(60));
    console.log("Scenario: Job exists with subject but empty metadata");
    console.log("Expected: Should still match by subject");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {}, // Empty metadata!
        },
    });
    console.log(`   Created job with empty metadata`);
    console.log(`   Subject: ${job1.subject}`);

    // Grab with artist/album names
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "subject-only-download",
        "subject-only-mbid",
        TEST_ALBUM,
        TEST_ARTIST,
        33333
    );

    const testJobs = await prisma.downloadJob.findMany({
        where: { subject: { contains: "Test Artist Dedup" } },
    });

    const passed = testJobs.length === 1 && grabResult.matched && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 11 PASSED: Subject-only matching works");
    } else {
        console.log("\n[FAIL] TEST 11 FAILED: Subject-only matching broken");
        console.log(`   Jobs found: ${testJobs.length}`);
    }
    
    return passed;
}

// Test 12: Three duplicate jobs - all should be merged
async function test12_ThreeDuplicatesMerge(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 12: Three Duplicate Jobs All Merge on Completion");
    console.log("=".repeat(60));
    console.log("Scenario: Three jobs exist for same album, one completes");
    console.log("Expected: All three should be marked as completed");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "three-dup-download",
            metadata: { artistName: TEST_ARTIST, albumTitle: TEST_ALBUM },
        },
    });
    const job2 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_2,
            status: "processing",
            metadata: { artistName: TEST_ARTIST, albumTitle: TEST_ALBUM },
        },
    });
    const job3 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: "third-mbid-" + Date.now(),
            status: "processing",
            metadata: { artistName: TEST_ARTIST, albumTitle: TEST_ALBUM },
        },
    });
    console.log(`   Created 3 duplicate jobs: ${job1.id}, ${job2.id}, ${job3.id}`);

    // Complete one
    await simpleDownloadManager.onDownloadComplete(
        "three-dup-download",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        22222
    );

    const testJobs = await prisma.downloadJob.findMany({
        where: { subject: { contains: "Test Artist Dedup" } },
    });

    const completedCount = testJobs.filter(j => j.status === "completed").length;
    const passed = completedCount === 3;
    
    if (passed) {
        console.log("\n[PASS] TEST 12 PASSED: All 3 duplicates merged");
    } else {
        console.log("\n[FAIL] TEST 12 FAILED: Not all duplicates merged");
        console.log(`   Completed: ${completedCount}/3`);
    }
    
    return passed;
}

// Test 13: Whitespace variations - should match after trimming
async function test13_WhitespaceVariations(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 13: Whitespace Variations");
    console.log("=".repeat(60));
    console.log("Scenario: Job has '  Artist  -  Album  ', grab comes with 'Artist - Album'");
    console.log("Expected: Should match after trimming whitespace");

    const PADDED_ARTIST = `  ${TEST_ARTIST}  `;
    const PADDED_ALBUM = `  ${TEST_ALBUM}  `;

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${PADDED_ARTIST} - ${PADDED_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: PADDED_ARTIST,
                albumTitle: PADDED_ALBUM,
            },
        },
    });
    console.log(`   Created job with padded names`);
    console.log(`   Artist: "${PADDED_ARTIST}"`);
    console.log(`   Album: "${PADDED_ALBUM}"`);

    // Grab with trimmed names
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        "whitespace-download",
        "whitespace-mbid",
        TEST_ALBUM.trim(),
        TEST_ARTIST.trim(),
        11111
    );

    console.log(`   Grabbed with trimmed names`);
    console.log(`   Matched: ${grabResult.matched}`);

    const testJobs = await prisma.downloadJob.findMany({
        where: { subject: { contains: "Test Artist Dedup" } },
    });

    const passed = testJobs.length === 1 && grabResult.matched && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 13 PASSED: Whitespace handling works");
    } else {
        console.log("\n[FAIL] TEST 13 FAILED: Whitespace not handled");
        console.log(`   Jobs: ${testJobs.length}, Matched: ${grabResult.matched}`);
    }
    
    return passed;
}

// Test 14: Special characters and Unicode
async function test14_SpecialCharacters(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 14: Special Characters and Unicode");
    console.log("=".repeat(60));
    console.log("Scenario: Artist/album names with Unicode, accents, special chars");
    console.log("Expected: Should match exactly");

    const UNICODE_ARTIST = "Röyksopp & björk 日本語";
    const UNICODE_ALBUM = "Mélodie d'amour (Remastered™) [Deluxe]";

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${UNICODE_ARTIST} - ${UNICODE_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: UNICODE_ARTIST,
                albumTitle: UNICODE_ALBUM,
            },
        },
    });
    console.log(`   Created job: ${job1.id}`);
    console.log(`   Artist: "${UNICODE_ARTIST}"`);
    console.log(`   Album: "${UNICODE_ALBUM}"`);

    // Grab with same Unicode names (use unique IDs to avoid matching old data)
    const uniqueId = Date.now();
    const grabResult = await simpleDownloadManager.onDownloadGrabbed(
        `unicode-download-${uniqueId}`,
        `unicode-mbid-${uniqueId}`,
        UNICODE_ALBUM,
        UNICODE_ARTIST,
        uniqueId % 100000 // Use a unique lidarrAlbumId
    );

    console.log(`   Matched: ${grabResult.matched}`);
    console.log(`   Matched to original job: ${grabResult.jobId === job1.id}`);

    // Verify by fetching the job directly
    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    
    // The key assertion: did it match the original job?
    const passed = grabResult.matched && grabResult.jobId === job1.id;
    
    if (passed) {
        console.log("\n[PASS] TEST 14 PASSED: Unicode/special chars handled");
    } else {
        console.log("\n[FAIL] TEST 14 FAILED: Unicode/special chars not handled");
        console.log(`   Expected jobId: ${job1.id}, Got: ${grabResult.jobId}`);
        console.log(`   lidarrRef: ${jobAfter?.lidarrRef}`);
    }
    
    return passed;
}

// Test 15: Concurrent race condition - multiple grabs at once
async function test15_ConcurrentGrabs(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 15: Concurrent Race Condition - Multiple Simultaneous Grabs");
    console.log("=".repeat(60));
    console.log("Scenario: Three grab events fire at nearly the same time for same album");
    console.log("Expected: At least one should link, NO duplicate jobs created");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created initial job: ${job1.id}`);

    // Fire 3 grabs concurrently (simulating race condition)
    console.log("   Firing 3 concurrent grabs...");
    const [result1, result2, result3] = await Promise.all([
        simpleDownloadManager.onDownloadGrabbed(
            "race-download-1",
            "race-mbid-1",
            TEST_ALBUM,
            TEST_ARTIST,
            9001
        ),
        simpleDownloadManager.onDownloadGrabbed(
            "race-download-2",
            "race-mbid-2",
            TEST_ALBUM,
            TEST_ARTIST,
            9002
        ),
        simpleDownloadManager.onDownloadGrabbed(
            "race-download-3",
            "race-mbid-3",
            TEST_ALBUM,
            TEST_ARTIST,
            9003
        ),
    ]);

    console.log(`   Result 1: matched=${result1.matched}, jobId=${result1.jobId}`);
    console.log(`   Result 2: matched=${result2.matched}, jobId=${result2.jobId}`);
    console.log(`   Result 3: matched=${result3.matched}, jobId=${result3.jobId}`);

    const testJobs = await prisma.downloadJob.findMany({
        where: { subject: { contains: "Test Artist Dedup" } },
    });

    // The KEY thing is: NO DUPLICATES created
    // At least one grab should match the original job
    // Others might not match (because job already has lidarrRef) - that's OK
    const atLeastOneMatched = result1.matched || result2.matched || result3.matched;
    const matchedToOriginal = result1.jobId === job1.id || result2.jobId === job1.id || result3.jobId === job1.id;
    const noDuplicates = testJobs.length === 1; // Only our original job
    
    const passed = atLeastOneMatched && matchedToOriginal && noDuplicates;
    
    if (passed) {
        console.log("\n[PASS] TEST 15 PASSED: No duplicates created under race condition");
        console.log(`   Jobs in DB: ${testJobs.length} (expected 1)`);
    } else {
        console.log("\n[FAIL] TEST 15 FAILED: Race condition issue");
        console.log(`   Jobs in DB: ${testJobs.length} (expected 1)`);
        console.log(`   At least one matched: ${atLeastOneMatched}`);
        console.log(`   Matched original: ${matchedToOriginal}`);
    }
    
    return passed;
}

// Test 16: Reconciliation function
async function test16_Reconciliation(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 16: Reconciliation Function");
    console.log("=".repeat(60));
    console.log("Scenario: Job stuck in 'processing' but album exists in Lidarr");
    console.log("Expected: reconcileWithLidarr should mark as completed");

    // Create a job that's "stuck" in processing
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            createdAt: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created stuck job: ${job1.id} (1 hour old)`);

    // Run reconciliation
    console.log("   Running reconcileWithLidarr()...");
    const result = await simpleDownloadManager.reconcileWithLidarr();
    console.log(`   Reconciled: ${result.reconciled}, Errors: ${result.errors.length}`);

    // Note: This test may not fully pass without mocking Lidarr API
    // But we verify the function runs without crashing
    const passed = typeof result.reconciled === "number" && Array.isArray(result.errors);
    
    if (passed) {
        console.log("\n[PASS] TEST 16 PASSED: Reconciliation function works");
    } else {
        console.log("\n[FAIL] TEST 16 FAILED: Reconciliation error");
    }
    
    return passed;
}

// Test 17: Stale job timeout
async function test17_StaleJobTimeout(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 17: Stale Job Timeout Detection");
    console.log("=".repeat(60));
    console.log("Scenario: Job created > 2 hours ago, still processing");
    console.log("Expected: markStaleJobsAsFailed should handle it");

    // Create a very old job
    const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
    
    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            createdAt: twoHoursAgo,
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
            },
        },
    });
    console.log(`   Created old job: ${job1.id} (2 hours ago)`);

    // Run stale job cleanup
    console.log("   Running markStaleJobsAsFailed()...");
    const result = await simpleDownloadManager.markStaleJobsAsFailed();
    console.log(`   Timed out: ${result.timedOut}`);

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    
    // Job should be marked as failed or exhausted
    const passed = jobAfter?.status === "failed" || jobAfter?.status === "exhausted" || result.timedOut > 0;
    
    if (passed) {
        console.log("\n[PASS] TEST 17 PASSED: Stale job timeout handled");
        console.log(`   Job status: ${jobAfter?.status}`);
    } else {
        console.log("\n[FAIL] TEST 17 FAILED: Stale job not timed out");
        console.log(`   Job status: ${jobAfter?.status}`);
    }
    
    return passed;
}

// Test 18: Spotify import jobs should NOT send notifications
async function test18_SpotifyImportNoNotification(): Promise<boolean> {
    console.log("\n" + "=".repeat(60));
    console.log("TEST 18: Spotify Import Jobs Should NOT Send Notifications");
    console.log("=".repeat(60));
    console.log("Scenario: Spotify import job completes");
    console.log("Expected: notificationSent should remain false (skipped)");

    const job1 = await prisma.downloadJob.create({
        data: {
            userId: TEST_USER_ID,
            subject: `${TEST_ARTIST} - ${TEST_ALBUM}`,
            type: "album",
            targetMbid: TEST_MBID_1,
            status: "processing",
            lidarrRef: "spotify-import-download",
            metadata: {
                artistName: TEST_ARTIST,
                albumTitle: TEST_ALBUM,
                spotifyImportJobId: "spotify-import-123", // This marks it as a Spotify import
                notificationSent: false,
            },
        },
    });
    console.log(`   Created Spotify import job: ${job1.id}`);

    await simpleDownloadManager.onDownloadComplete(
        "spotify-import-download",
        TEST_MBID_1,
        TEST_ARTIST,
        TEST_ALBUM,
        44444
    );

    const jobAfter = await prisma.downloadJob.findUnique({ where: { id: job1.id } });
    const meta = jobAfter?.metadata as any;
    
    // notificationSent should NOT be true for Spotify import jobs
    const passed = jobAfter?.status === "completed" && meta?.notificationSent !== true;
    
    if (passed) {
        console.log("\n[PASS] TEST 18 PASSED: Spotify import notification correctly skipped");
    } else {
        console.log("\n[FAIL] TEST 18 FAILED: Spotify import incorrectly sent notification");
        console.log(`   notificationSent: ${meta?.notificationSent}`);
    }
    
    return passed;
}

async function runAllTests() {
    console.log("\n" + "=".repeat(60));
    console.log("DOWNLOAD JOB DEDUPLICATION TEST SUITE");
    console.log("=".repeat(60));

    const results: { name: string; passed: boolean }[] = [];

    try {
        // Setup: Get real user ID
        await setup();
        
        console.log(`Test User ID: ${TEST_USER_ID}`);
        console.log(`Test Artist: ${TEST_ARTIST}`);
        console.log(`Test Album: ${TEST_ALBUM}`);

        // Test 1: Duplicate detection on grab
        await cleanup();
        results.push({ 
            name: "Duplicate Job Detection on Grab", 
            passed: await test1_DuplicateJobDetectionOnGrab() 
        });

        // Test 2: Completion merges duplicates
        await cleanup();
        results.push({ 
            name: "Completion Merges Duplicates", 
            passed: await test2_CompletionMergesDuplicates() 
        });

        // Test 3: Notification dedup
        await cleanup();
        results.push({ 
            name: "Notification Deduplication", 
            passed: await test3_NotificationDedup() 
        });

        // Test 4: Grab matches pending by name
        await cleanup();
        results.push({ 
            name: "Grab Matches Pending by Name", 
            passed: await test4_GrabMatchesByNameWhenMbidDiffers() 
        });

        // Test 5: Completion matches by name
        await cleanup();
        results.push({ 
            name: "Completion Matches by Name", 
            passed: await test5_CompletionMatchesByNameWhenNoLidarrRef() 
        });

        // Test 6: Case-insensitive matching
        await cleanup();
        results.push({ 
            name: "Case-Insensitive Matching", 
            passed: await test6_CaseInsensitiveMatching() 
        });

        // Test 7: Same artist, different album
        await cleanup();
        results.push({ 
            name: "Same Artist Different Album - No Match", 
            passed: await test7_SameArtistDifferentAlbum() 
        });

        // Test 8: Idempotent completion
        await cleanup();
        results.push({ 
            name: "Idempotent Completion", 
            passed: await test8_IdempotentCompletion() 
        });

        // Test 9: Discovery jobs no notification
        await cleanup();
        results.push({ 
            name: "Discovery Jobs Skip Notification", 
            passed: await test9_DiscoveryJobsNoNotification() 
        });

        // Test 10: Retry updates lidarrRef
        await cleanup();
        results.push({ 
            name: "Retry Updates lidarrRef", 
            passed: await test10_RetryUpdatesLidarrRef() 
        });

        // Test 11: Subject-only matching
        await cleanup();
        results.push({ 
            name: "Subject-Only Matching", 
            passed: await test11_SubjectOnlyMatching() 
        });

        // Test 12: Three duplicates merge
        await cleanup();
        results.push({ 
            name: "Three Duplicates All Merge", 
            passed: await test12_ThreeDuplicatesMerge() 
        });

        // Test 13: Whitespace variations
        await cleanup();
        results.push({ 
            name: "Whitespace Variations", 
            passed: await test13_WhitespaceVariations() 
        });

        // Test 14: Special characters and Unicode
        await cleanup();
        results.push({ 
            name: "Special Characters and Unicode", 
            passed: await test14_SpecialCharacters() 
        });

        // Test 15: Concurrent race condition
        await cleanup();
        results.push({ 
            name: "Concurrent Race Condition", 
            passed: await test15_ConcurrentGrabs() 
        });

        // Test 16: Reconciliation function
        await cleanup();
        results.push({ 
            name: "Reconciliation Function", 
            passed: await test16_Reconciliation() 
        });

        // Test 17: Stale job timeout
        await cleanup();
        results.push({ 
            name: "Stale Job Timeout", 
            passed: await test17_StaleJobTimeout() 
        });

        // Test 18: Spotify import no notification
        await cleanup();
        results.push({ 
            name: "Spotify Import Skip Notification", 
            passed: await test18_SpotifyImportNoNotification() 
        });

    } catch (error) {
        console.error("\n Test execution error:", error);
    } finally {
        await cleanup();
        await prisma.$disconnect();
    }

    // Summary
    console.log("\n" + "=".repeat(60));
    console.log("TEST RESULTS SUMMARY");
    console.log("=".repeat(60));
    
    let passedCount = 0;
    let failedCount = 0;
    
    for (const result of results) {
        const icon = result.passed ? "PASS" : "FAIL";
        console.log(`${icon} ${result.name}`);
        if (result.passed) passedCount++;
        else failedCount++;
    }

    console.log("\n" + "-".repeat(60));
    console.log(`Total: ${results.length} tests`);
    console.log(`Passed: ${passedCount}`);
    console.log(`Failed: ${failedCount}`);
    
    if (failedCount === 0) {
        console.log("\n🎉 ALL TESTS PASSED! Download deduplication is working correctly.");
    } else {
        console.log("\n💥 SOME TESTS FAILED. Review the output above for details.");
    }

    process.exit(failedCount > 0 ? 1 : 0);
}

// Run tests
runAllTests();

