import { describe, expect, it } from 'vitest';
import { onInvoke } from '@/test/mocks/tauri';
import type { SearchResults } from '@/lib/api/subsonicTypes';
import { useAuthStore } from '@/store/authStore';
import {
  liveSearchQueryRejected,
  liveSearchQueryTooShort,
  mergeLiveSearchResults,
  runLocalLiveSearch,
} from './liveSearchLocal';

const neverStale = { epoch: 1, isStale: () => false };
const alwaysStale = { epoch: 1, isStale: () => true };

describe('runLocalLiveSearch', () => {
  it('returns null without invoking for a single-character query', async () => {
    let invoked = false;
    onInvoke('library_live_search', () => {
      invoked = true;
      return { artists: [], albums: [], tracks: [], source: 'local' };
    });
    await expect(runLocalLiveSearch('s1', 'а', neverStale)).resolves.toBeNull();
    expect(invoked).toBe(false);
  });

  it('returns null when stale before invoke completes', async () => {
    onInvoke('library_live_search', () => ({
      artists: [],
      albums: [],
      tracks: [{ serverId: 's1', id: 't1', title: 'T', album: 'A', durationSec: 1, syncedAt: 0 }],
      source: 'local',
    }));
    await expect(runLocalLiveSearch('s1', 'foo', alwaysStale)).resolves.toBeNull();
  });

  it('returns null when live search invoke fails', async () => {
    onInvoke('library_live_search', () => {
      throw new Error('boom');
    });
    await expect(runLocalLiveSearch('s1', 'foo', neverStale)).resolves.toBeNull();
  });

  it('maps live search rows to search3-shaped limits', async () => {
    onInvoke('library_live_search', () => ({
      artists: Array.from({ length: 8 }, (_, i) => ({
        serverId: 's1',
        id: `a${i}`,
        name: `Artist ${i}`,
        albumCount: 2,
        syncedAt: 1,
        rawJson: {},
      })),
      albums: Array.from({ length: 7 }, (_, i) => ({
        serverId: 's1',
        id: `al${i}`,
        name: `Album ${i}`,
        artist: 'A',
        artistId: 'a0',
        songCount: 1,
        durationSec: 100,
        syncedAt: 1,
        rawJson: {},
      })),
      tracks: Array.from({ length: 12 }, (_, i) => ({
        serverId: 's1',
        id: `t${i}`,
        title: `Track ${i}`,
        artist: 'A',
        album: 'Al',
        durationSec: 200,
        syncedAt: 1,
        rawJson: { id: `t${i}`, title: `Track ${i}`, artist: 'A', album: 'Al', albumId: 'al0', duration: 200 },
      })),
      source: 'local',
    }));

    const res = await runLocalLiveSearch('s1', 'foo', neverStale);
    expect(res).not.toBeNull();
    expect(res!.artists).toHaveLength(5);
    expect(res!.albums).toHaveLength(5);
    expect(res!.songs).toHaveLength(10);
  });

  it('passes libraryScope from the sidebar music library filter', async () => {
    useAuthStore.setState({ musicLibraryFilterByServer: { s1: 'lib7' } });
    let captured: unknown;
    onInvoke('library_live_search', (args) => {
      captured = args;
      return { artists: [], albums: [], tracks: [], source: 'local' };
    });
    await runLocalLiveSearch('s1', 'foo', neverStale);
    expect(captured).toMatchObject({
      request: {
        serverId: 's1',
        libraryScope: 'lib7',
        libraryScopes: [{ serverId: 's1', libraryId: 'lib7' }],
      },
    });
  });

  it('passes ordered libraryScopes for multi-library selection', async () => {
    useAuthStore.setState({
      musicLibrarySelectionByServer: { s1: ['lib-b', 'lib-a'] },
      musicLibraryFilterByServer: { s1: 'lib-b' },
    });
    let captured: unknown;
    onInvoke('library_live_search', (args) => {
      captured = args;
      return { artists: [], albums: [], tracks: [], source: 'local' };
    });
    await runLocalLiveSearch('s1', 'foo', neverStale);
    expect(captured).toMatchObject({
      request: {
        libraryScopes: [
          { serverId: 's1', libraryId: 'lib-b' },
          { serverId: 's1', libraryId: 'lib-a' },
        ],
      },
    });
  });
});

describe('liveSearchQueryRejected', () => {
  it('rejects syntax junk and single-character queries', () => {
    expect(liveSearchQueryRejected('**')).toBe(true);
    expect(liveSearchQueryRejected('1=2')).toBe(true);
    expect(liveSearchQueryRejected('а')).toBe(true);
    expect(liveSearchQueryRejected('ab')).toBe(false);
    expect(liveSearchQueryRejected('metallica')).toBe(false);
  });
});

describe('liveSearchQueryTooShort', () => {
  it('treats one grapheme as too short', () => {
    expect(liveSearchQueryTooShort('а')).toBe(true);
    expect(liveSearchQueryTooShort('ab')).toBe(false);
  });
});

describe('mergeLiveSearchResults', () => {
  it('keeps local order and fills gaps from network', () => {
    const local: SearchResults = {
      artists: [{ id: 'a1', name: 'Local' }],
      albums: [],
      songs: [{ id: 's1', title: 'Song', artist: 'A', album: 'Al', albumId: 'al0', duration: 1 }],
    };
    const network: SearchResults = {
      artists: [{ id: 'a2', name: 'Net' }],
      albums: [{ id: 'al1', name: 'Album', artist: 'A', artistId: 'a2', songCount: 1, duration: 100 }],
      songs: [{ id: 's2', title: 'Other', artist: 'B', album: 'Bl', albumId: 'al1', duration: 2 }],
    };
    const merged = mergeLiveSearchResults(local, network);
    expect(merged.artists.map(a => a.id)).toEqual(['a1', 'a2']);
    expect(merged.albums.map(a => a.id)).toEqual(['al1']);
    expect(merged.songs.map(s => s.id)).toEqual(['s1', 's2']);
  });
});
