/**
 * Component: Request Creator Ignore Tests
 * Documentation: documentation/features/ignored-audiobooks.md
 *
 * Tests the per-user ignore list check in createRequestForUser,
 * including direct ASIN match, works-system sibling expansion,
 * and the bypassIgnore option.
 */

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../helpers/prisma';

const prismaMock = createPrismaMock();

vi.mock('@/lib/db', () => ({
  prisma: prismaMock,
}));

vi.mock('@/lib/utils/logger', () => ({
  RMABLogger: {
    create: () => ({
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    }),
  },
}));

// Mock findPlexMatch to return null (not in library)
vi.mock('@/lib/utils/audiobook-matcher', () => ({
  findPlexMatch: vi.fn().mockResolvedValue(null),
}));

// Mock AudibleService (default = no Audnexus data)
const audibleServiceMock = vi.hoisted(() => ({
  getAudiobookDetails: vi.fn().mockResolvedValue(null),
}));
vi.mock('@/lib/integrations/audible.service', () => ({
  getAudibleService: () => audibleServiceMock,
}));

// Mock job queue (shared across tests so we can assert addSearchJob calls)
const jobQueueAddSearchJob = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const jobQueueAddNotificationJob = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
vi.mock('@/lib/services/job-queue.service', () => ({
  getJobQueueService: () => ({
    addSearchJob: jobQueueAddSearchJob,
    addNotificationJob: jobQueueAddNotificationJob,
  }),
}));

// Mock config service for indexer.skip_unreleased setting
const configServiceGet = vi.hoisted(() => vi.fn());
vi.mock('@/lib/services/config.service', () => ({
  getConfigService: () => ({ get: configServiceGet }),
}));

// Mock getSiblingAsins from works.service
const mockGetSiblingAsins = vi.fn().mockResolvedValue(new Map());
const mockSeedAsin = vi.fn().mockResolvedValue(undefined);

vi.mock('@/lib/services/works.service', () => ({
  getSiblingAsins: (...args: any[]) => mockGetSiblingAsins(...args),
  seedAsin: (...args: any[]) => mockSeedAsin(...args),
}));

const TEST_AUDIOBOOK = {
  asin: 'B00TEST001',
  title: 'Test Book',
  author: 'Test Author',
};

const TEST_USER_ID = 'user-123';

describe('createRequestForUser — ignore list', () => {
  beforeEach(() => {
    vi.clearAllMocks();

    // Restore mock return values cleared by clearAllMocks
    jobQueueAddSearchJob.mockResolvedValue(undefined);
    jobQueueAddNotificationJob.mockResolvedValue(undefined);

    // Default: no existing requests, no library matches
    prismaMock.request.findFirst.mockResolvedValue(null);
    prismaMock.audiobook.findFirst.mockResolvedValue(null);
    prismaMock.audiobook.create.mockResolvedValue({
      id: 'audiobook-1',
      audibleAsin: TEST_AUDIOBOOK.asin,
      title: TEST_AUDIOBOOK.title,
      author: TEST_AUDIOBOOK.author,
      narrator: null,
    });
    prismaMock.request.create.mockResolvedValue({
      id: 'request-1',
      userId: TEST_USER_ID,
      audiobookId: 'audiobook-1',
      status: 'pending',
      audiobook: { id: 'audiobook-1', title: 'Test Book' },
      user: { id: TEST_USER_ID, plexUsername: 'testuser' },
    });
    prismaMock.user.findUnique.mockResolvedValue({
      role: 'user',
      autoApproveRequests: true,
      plexUsername: 'testuser',
    });

    // Default: not ignored
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);
    prismaMock.ignoredAudiobook.findFirst.mockResolvedValue(null);
    mockGetSiblingAsins.mockResolvedValue(new Map());
    mockSeedAsin.mockResolvedValue(undefined);

    // Default Audnexus + config behaviour
    audibleServiceMock.getAudiobookDetails.mockResolvedValue(null);
    configServiceGet.mockResolvedValue(null); // default → setting ON
  });

  it('blocks auto-request when ASIN is directly ignored', async () => {
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue({
      id: 'ignored-1',
      userId: TEST_USER_ID,
      asin: TEST_AUDIOBOOK.asin,
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.reason).toBe('ignored');
      expect(result.message).toContain('ignore list');
    }

    // Should NOT create a request
    expect(prismaMock.request.create).not.toHaveBeenCalled();
  });

  it('blocks auto-request when sibling ASIN is ignored', async () => {
    // Direct ASIN not ignored
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);

    // But a sibling is ignored
    mockGetSiblingAsins.mockResolvedValue(new Map([
      [TEST_AUDIOBOOK.asin, ['B00SIBLING']],
    ]));
    prismaMock.ignoredAudiobook.findFirst.mockResolvedValue({
      id: 'ignored-sibling',
      userId: TEST_USER_ID,
      asin: 'B00SIBLING',
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.reason).toBe('ignored');
    }

    expect(prismaMock.request.create).not.toHaveBeenCalled();
  });

  it('allows manual request with bypassIgnore even when ignored', async () => {
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue({
      id: 'ignored-1',
      userId: TEST_USER_ID,
      asin: TEST_AUDIOBOOK.asin,
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK, {
      bypassIgnore: true,
    });

    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalled();

    // Should NOT have even checked the ignore list
    expect(prismaMock.ignoredAudiobook.findUnique).not.toHaveBeenCalled();
  });

  it('allows request when ASIN is not ignored', async () => {
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);
    prismaMock.ignoredAudiobook.findFirst.mockResolvedValue(null);

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalled();
  });

  it('falls through gracefully when works expansion fails', async () => {
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);
    mockGetSiblingAsins.mockRejectedValue(new Error('DB error'));

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    // Should still succeed since direct check passed and expansion is best-effort
    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalled();
  });

  it('does not check siblings when no sibling ASINs exist', async () => {
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);
    mockGetSiblingAsins.mockResolvedValue(new Map());

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(true);
    // Should not have queried findFirst for sibling check since map was empty
    expect(prismaMock.ignoredAudiobook.findFirst).not.toHaveBeenCalled();
  });
});

describe('createRequestForUser — release-date gate', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    jobQueueAddSearchJob.mockResolvedValue(undefined);
    jobQueueAddNotificationJob.mockResolvedValue(undefined);
    prismaMock.request.findFirst.mockResolvedValue(null);
    prismaMock.audiobook.findFirst.mockResolvedValue(null);
    prismaMock.audiobook.create.mockResolvedValue({
      id: 'audiobook-1',
      audibleAsin: TEST_AUDIOBOOK.asin,
      title: TEST_AUDIOBOOK.title,
      author: TEST_AUDIOBOOK.author,
      narrator: null,
    });
    prismaMock.user.findUnique.mockResolvedValue({
      role: 'user',
      autoApproveRequests: true,
      plexUsername: 'testuser',
    });
    prismaMock.ignoredAudiobook.findUnique.mockResolvedValue(null);
    prismaMock.ignoredAudiobook.findFirst.mockResolvedValue(null);
    mockGetSiblingAsins.mockResolvedValue(new Map());
    mockSeedAsin.mockResolvedValue(undefined);
  });

  it('creates request in awaiting_release with no search when book is unreleased and setting ON', async () => {
    const future = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
    audibleServiceMock.getAudiobookDetails.mockResolvedValue({ releaseDate: future });
    configServiceGet.mockResolvedValue(null); // default → ON

    prismaMock.request.create.mockResolvedValue({
      id: 'request-future-on',
      userId: TEST_USER_ID,
      audiobookId: 'audiobook-1',
      status: 'awaiting_release',
      audiobook: { id: 'audiobook-1', title: 'Test Book' },
      user: { id: TEST_USER_ID, plexUsername: 'testuser' },
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: 'awaiting_release',
          releaseDate: expect.any(Date),
        }),
      })
    );
    expect(jobQueueAddSearchJob).not.toHaveBeenCalled();
  });

  it('creates pending request and runs search when book is already released and setting ON', async () => {
    const past = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString();
    audibleServiceMock.getAudiobookDetails.mockResolvedValue({ releaseDate: past });
    configServiceGet.mockResolvedValue('true');

    prismaMock.request.create.mockResolvedValue({
      id: 'request-past-on',
      userId: TEST_USER_ID,
      audiobookId: 'audiobook-1',
      status: 'pending',
      audiobook: { id: 'audiobook-1', title: 'Test Book' },
      user: { id: TEST_USER_ID, plexUsername: 'testuser' },
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: 'pending',
        }),
      })
    );
    expect(jobQueueAddSearchJob).toHaveBeenCalled();
  });

  it('creates pending request and runs search when book is unreleased but setting OFF', async () => {
    const future = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
    audibleServiceMock.getAudiobookDetails.mockResolvedValue({ releaseDate: future });
    configServiceGet.mockResolvedValue('false');

    prismaMock.request.create.mockResolvedValue({
      id: 'request-future-off',
      userId: TEST_USER_ID,
      audiobookId: 'audiobook-1',
      status: 'pending',
      audiobook: { id: 'audiobook-1', title: 'Test Book' },
      user: { id: TEST_USER_ID, plexUsername: 'testuser' },
    });

    const { createRequestForUser } = await import('@/lib/services/request-creator.service');
    const result = await createRequestForUser(TEST_USER_ID, TEST_AUDIOBOOK);

    expect(result.success).toBe(true);
    expect(prismaMock.request.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: 'pending',
        }),
      })
    );
    expect(jobQueueAddSearchJob).toHaveBeenCalled();
  });
});
