import {describe, expect, it} from 'vitest'

import {
  assembleQueryText,
  recommendationsErrorMessage,
  recommendationsQuery,
  recommendedConceptIds,
  toConceptRecommendations,
} from './semanticRecommendations'

describe('recommendationsQuery', () => {
  const query = recommendationsQuery()

  it('matches the characterized GROQ output', () => {
    expect(query).toMatchSnapshot()
  })

  it('scores skosConcepts by semanticSimilarity and returns the top $maxResults', () => {
    expect(query).toContain('_type == "skosConcept"')
    expect(query).toContain('score(text::semanticSimilarity($searchQuery))')
    expect(query).toContain('order(_score desc)')
    expect(query).toContain('[0...$maxResults]')
    expect(query).toContain('"conceptId": _id')
    expect(query).toContain('"score": _score')
  })

  it("scopes candidates to the field's scheme via $schemeId membership", () => {
    expect(query).toContain('_type == "skosConceptScheme" && schemeId == $schemeId')
    expect(query).toContain('topConcepts[]._ref')
    expect(query).toContain('concepts[]._ref')
    expect(query).toContain('_id in')
  })

  it('uses no part of the deprecated Embeddings Index API', () => {
    expect(query).not.toContain('text::embedding')
    expect(query).not.toContain('embeddings-index')
  })
})

describe('assembleQueryText', () => {
  it('joins the non-empty field values with a space, in order', () => {
    const text = assembleQueryText([
      {name: 'title', value: 'Summer'},
      {name: 'description', value: 'reading list'},
    ])
    expect(text).toBe('Summer reading list')
  })

  it('throws naming the single empty field', () => {
    expect(() =>
      assembleQueryText([
        {name: 'title', value: 'Summer'},
        {name: 'description', value: ''},
      ])
    ).toThrowError('Please fill out the description field to enable match scores.')
  })

  it('throws listing every empty field when more than one is empty', () => {
    expect(() =>
      assembleQueryText([
        {name: 'title', value: ''},
        {name: 'description', value: '   '},
      ])
    ).toThrowError(
      'The following fields must be filled out to enable match scores: title, description'
    )
  })

  it('treats whitespace-only and non-string values as empty', () => {
    expect(() =>
      assembleQueryText([
        {name: 'title', value: '  '},
        {name: 'count', value: 42},
      ])
    ).toThrowError(
      'The following fields must be filled out to enable match scores: title, count'
    )
  })
})

describe('toConceptRecommendations', () => {
  it('maps rows to {conceptId, score}', () => {
    const recs = toConceptRecommendations([
      {conceptId: 'concept-a', score: 0.9},
      {conceptId: 'concept-b', score: 0.4},
    ])
    expect(recs).toEqual([
      {conceptId: 'concept-a', score: 0.9},
      {conceptId: 'concept-b', score: 0.4},
    ])
  })

  it('normalizes draft and version ids to their published form', () => {
    const recs = toConceptRecommendations([
      {conceptId: 'drafts.concept-a', score: 0.9},
      {conceptId: 'versions.rABC.concept-b', score: 0.4},
    ])
    expect(recs).toEqual([
      {conceptId: 'concept-a', score: 0.9},
      {conceptId: 'concept-b', score: 0.4},
    ])
  })

  it('drops rows missing a string id or numeric score', () => {
    const recs = toConceptRecommendations([
      {conceptId: 'concept-a', score: 0.9},
      {conceptId: '', score: 0.5},
      {conceptId: 'concept-c', score: null as unknown as number},
      null as unknown as {conceptId: string; score: number},
    ])
    expect(recs).toEqual([{conceptId: 'concept-a', score: 0.9}])
  })

  it('returns an empty array for null or undefined input', () => {
    expect(toConceptRecommendations(null)).toEqual([])
    expect(toConceptRecommendations(undefined)).toEqual([])
  })
})

describe('recommendedConceptIds', () => {
  it('collects the recommended concept ids as a set', () => {
    const ids = recommendedConceptIds([
      {conceptId: 'a', score: 0.9},
      {conceptId: 'b', score: 0.4},
    ])
    expect(ids).toEqual(new Set(['a', 'b']))
  })

  it('de-duplicates repeated concept ids', () => {
    const ids = recommendedConceptIds([
      {conceptId: 'a', score: 0.9},
      {conceptId: 'a', score: 0.3},
      {conceptId: 'b', score: 0.5},
    ])
    expect(ids).toEqual(new Set(['a', 'b']))
  })

  it('returns an empty set for no recommendations', () => {
    expect(recommendedConceptIds([]).size).toBe(0)
  })
})

describe('recommendationsErrorMessage', () => {
  it('flags an embeddings-related error as the dataset not being enabled', () => {
    const message = recommendationsErrorMessage(
      new Error('text::semanticSimilarity requires embeddings to be enabled')
    )
    expect(message).toBe("Semantic recommendations aren't enabled for this dataset.")
  })

  it('falls back to a generic message for any other failure', () => {
    expect(recommendationsErrorMessage(new Error('Network request failed'))).toBe(
      'Unable to load semantic recommendations.'
    )
    expect(recommendationsErrorMessage('socket hang up')).toBe(
      'Unable to load semantic recommendations.'
    )
    expect(recommendationsErrorMessage(undefined)).toBe('Unable to load semantic recommendations.')
  })
})
