import { describe, it, expect, beforeEach, vi } from 'vitest';
import { onInvoke } from '@/test/mocks/tauri';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import type { TrackRefDto } from '@/lib/api/library';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import * as subsonic from '@/lib/api/subsonicLibrary';
import {
  resolveBatch,
  resolveVisibleRange,
  getCachedTrack,
  placeholderTrack,
  applyQueueOverrides,
  seedQueueResolver,
  invalidateQueueResolver,
  subscribeQueueResolver,
  _resetQueueResolverForTest,
} from './queueTrackResolver';

const ready = () =>
  onInvoke('library_get_status', () => ({
    serverId: 's1', libraryScope: '', syncPhase: 'ready',
    capabilityFlags: 0, libraryTier: 'unknown', syncedAt: 0,
  }));

const notReady = () =>
  onInvoke('library_get_status', () => ({ serverId: 's1', libraryScope: '', syncPhase: 'initial_sync' }));

const echoBatch = () =>
  onInvoke('library_get_tracks_batch', (args) =>
    (args as { refs: TrackRefDto[] }).refs.map(r => ({
      serverId: r.serverId, id: r.trackId, title: `T-${r.trackId}`,
      album: 'A', durationSec: 1, syncedAt: 0, rawJson: {},
    })),
  );

const ref = (trackId: string, extra: Partial<QueueItemRef> = {}): QueueItemRef => ({ serverId: 's1', trackId, ...extra });

describe('queueTrackResolver', () => {
  beforeEach(() => {
    _resetQueueResolverForTest();
    useLibraryIndexStore.setState({ masterEnabled: true });
    usePlayerStore.setState({ starredOverrides: {}, userRatingOverrides: {} });
    vi.restoreAllMocks();
  });

  it('getCachedTrack returns undefined on a miss (no fetch)', () => {
    expect(getCachedTrack(ref('x'))).toBeUndefined();
  });

  it('resolveBatch fills the cache from the index; getCachedTrack reads it', async () => {
    ready();
    echoBatch();
    await resolveBatch([ref('t1'), ref('t2')]);
    expect(getCachedTrack(ref('t1'))?.title).toBe('T-t1');
    expect(getCachedTrack(ref('t2'))?.title).toBe('T-t2');
  });

  it('carries queue-only flags from the ref onto the resolved track', async () => {
    ready();
    echoBatch();
    await resolveBatch([ref('t1', { radioAdded: true }), ref('t2', { autoAdded: true, playNextAdded: true })]);
    expect(getCachedTrack(ref('t1'))?.radioAdded).toBe(true);
    expect(getCachedTrack(ref('t2'))?.autoAdded).toBe(true);
    expect(getCachedTrack(ref('t2'))?.playNextAdded).toBe(true);
  });

  it('falls back to network getSongForServer when the index is not ready', async () => {
    notReady();
    const spy = vi.spyOn(subsonic, 'getSongForServer').mockResolvedValue({
      id: 't9', title: 'Net Song', album: 'A', duration: 1,
    } as never);
    await resolveBatch([ref('t9')]);
    expect(spy).toHaveBeenCalledWith('s1', 't9');
    expect(getCachedTrack(ref('t9'))?.title).toBe('Net Song');
  });

  it('notifies subscribers when a fetch lands', async () => {
    ready();
    echoBatch();
    const cb = vi.fn();
    const unsub = subscribeQueueResolver(cb);
    await resolveBatch([ref('t1')]);
    expect(cb).toHaveBeenCalled();
    unsub();
  });

  it('does not re-fetch already-cached refs', async () => {
    ready();
    const batch = vi.fn((args: { refs: TrackRefDto[] }) =>
      args.refs.map(r => ({ serverId: r.serverId, id: r.trackId, title: r.trackId, album: 'A', durationSec: 1, syncedAt: 0, rawJson: {} })));
    onInvoke('library_get_tracks_batch', batch as never);
    await resolveBatch([ref('t1')]);
    await resolveBatch([ref('t1')]); // cached → no second batch call
    expect(batch).toHaveBeenCalledTimes(1);
  });

  it('seedQueueResolver caches known tracks without a fetch', () => {
    seedQueueResolver('s1', [{ id: 't1', title: 'Seeded', artist: '', album: 'A', albumId: 'A', duration: 1 }]);
    expect(getCachedTrack(ref('t1'))?.title).toBe('Seeded');
  });

  it('seedQueueResolver keeps an entire large replacement queue in cache', () => {
    const tracks = Array.from({ length: 555 }, (_, i) => ({
      id: `bulk-${i}`,
      title: `Track ${i}`,
      artist: 'Artist',
      album: 'Album',
      albumId: '',
      duration: 200,
    }));
    seedQueueResolver('navidrome-public-share', tracks);
    expect(getCachedTrack(ref('bulk-0', { serverId: 'navidrome-public-share' }))?.title).toBe('Track 0');
    expect(getCachedTrack(ref('bulk-554', { serverId: 'navidrome-public-share' }))?.title).toBe('Track 554');
  });

  it('resolveBatch builds public share tracks from ref directStreamUrl', async () => {
    await resolveBatch([{
      serverId: 'navidrome-public-share',
      trackId: 'ndshare:Ab12:0',
      directStreamUrl: 'https://music.example.com/share/s/jwt-a',
      directCoverArtUrl: 'https://music.example.com/share/img/jwt-a?size=300',
    }]);
    expect(getCachedTrack({
      serverId: 'navidrome-public-share',
      trackId: 'ndshare:Ab12:0',
    })?.directStreamUrl).toBe('https://music.example.com/share/s/jwt-a');
  });

  it('single-track seed does not evict a bulk-seeded public share queue', () => {
    const tracks = Array.from({ length: 555 }, (_, i) => ({
      id: `bulk-${i}`,
      title: `Track ${i}`,
      artist: 'Artist',
      album: 'Album',
      albumId: '',
      duration: 200,
      directStreamUrl: `https://music.example.com/share/s/token-${i}`,
    }));
    seedQueueResolver('navidrome-public-share', tracks);
    seedQueueResolver('navidrome-public-share', [tracks[10]!]);
    expect(getCachedTrack(ref('bulk-0', { serverId: 'navidrome-public-share' }))?.title).toBe('Track 0');
    expect(getCachedTrack(ref('bulk-100', { serverId: 'navidrome-public-share' }))?.directStreamUrl)
      .toBe('https://music.example.com/share/s/token-100');
  });

  it('invalidateQueueResolver drops the cached entry', async () => {
    ready();
    echoBatch();
    await resolveBatch([ref('t1')]);
    expect(getCachedTrack(ref('t1'))).toBeDefined();
    invalidateQueueResolver('t1');
    expect(getCachedTrack(ref('t1'))).toBeUndefined();
  });

  it('applyQueueOverrides merges session star/rating overrides', () => {
    usePlayerStore.setState({ starredOverrides: { t1: true }, userRatingOverrides: { t1: 4 } });
    const merged = applyQueueOverrides({ id: 't1', title: 'X', artist: '', album: 'A', albumId: 'A', duration: 1 });
    expect(!!merged.starred).toBe(true);
    expect(merged.userRating).toBe(4);
  });

  it('applyQueueOverrides clears starred when the override is false', () => {
    usePlayerStore.setState({ starredOverrides: { t1: false }, userRatingOverrides: {} });
    const merged = applyQueueOverrides({ id: 't1', title: 'X', artist: '', album: 'A', albumId: 'A', duration: 1, starred: '2020' });
    expect(merged.starred).toBeUndefined();
  });

  it('placeholderTrack preserves identity + queue flags', () => {
    const p = placeholderTrack(ref('t1', { radioAdded: true }));
    expect(p.id).toBe('t1');
    expect(p.radioAdded).toBe(true);
  });

  it('resolveVisibleRange prefetches the window around the visible rows', async () => {
    ready();
    const batch = vi.fn((args: { refs: TrackRefDto[] }) =>
      args.refs.map(r => ({ serverId: r.serverId, id: r.trackId, title: r.trackId, album: 'A', durationSec: 1, syncedAt: 0, rawJson: {} })));
    onInvoke('library_get_tracks_batch', batch as never);
    const refs = Array.from({ length: 400 }, (_, i) => ref(`t${i}`));
    resolveVisibleRange(refs, 100, 120);
    await vi.waitFor(() => expect(getCachedTrack(ref('t120'))).toBeDefined());
    // back window: t50 in range (100 - 50); t49 out of range
    expect(getCachedTrack(ref('t50'))).toBeDefined();
    expect(getCachedTrack(ref('t49'))).toBeUndefined();
  });
});
