import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as decksApi from './decks.js'

const nextResults = []
const countResults = []
function createChain() {
  const getResult = () => nextResults.shift() ?? { data: null, error: null }
  const getCount = () => countResults.shift() ?? { count: 0, error: null }
  const countThenable = {
    eq: vi.fn(function () { return this }),
    then(resolve) { return Promise.resolve(getCount()).then(resolve) },
    catch(fn) { return Promise.resolve(getCount()).catch(fn) },
  }
  const chain = {
    from: vi.fn(function () { return this }),
    select: vi.fn(function (cols, opts) {
      if (opts?.count === 'exact' && opts?.head) return countThenable
      return this
    }),
    eq: vi.fn(function () { return this }),
    order: vi.fn(function () { return this }),
    in: vi.fn(function () { return this }),
    not: vi.fn(function () { return this }),
    limit: vi.fn(function () { return this }),
    or: vi.fn(function () { return this }),
    single: vi.fn(() => Promise.resolve(getResult())),
    insert: vi.fn(function () { return this }),
    update: vi.fn(function () { return this }),
    delete: vi.fn(function () { return this }),
    maybeSingle: vi.fn(() => Promise.resolve(getResult())),
    upsert: vi.fn(() => Promise.resolve(getResult())),
    then(resolve) { return Promise.resolve(getResult()).then(resolve) },
    catch(fn) { return Promise.resolve(getResult()).catch(fn) },
  }
  return chain
}

let fromImpl = () => createChain()
vi.mock('../supabase.js', () => ({
  supabase: {
    from: vi.fn((...args) => fromImpl(...args)),
  },
}))

describe('decks API', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    nextResults.length = 0
    countResults.length = 0
    fromImpl = () => createChain()
  })

  describe('fetchMyDecks', () => {
    it('returns empty array when no decks', async () => {
      nextResults.push({ data: [], error: null })
      const result = await decksApi.fetchMyDecks('user1')
      expect(result).toEqual([])
    })

    it('returns decks with question_count and needs_update', async () => {
      const decks = [
        { id: 'd1', title: 'Deck 1', copied_from_deck_id: null, copied_from_version: null, updated_at: '2025-01-01' },
      ]
      nextResults.push({ data: decks, error: null }, { data: [], error: null }, { data: [], error: null })
      countResults.push({ count: 3, error: null })
      const result = await decksApi.fetchMyDecks('user1')
      expect(result).toHaveLength(1)
      expect(result[0].question_count).toBe(3)
      expect(result[0].title).toBe('Deck 1')
    })
  })

  describe('fetchPublishedDecks', () => {
    it('returns empty array when no published decks', async () => {
      nextResults.push({ data: [], error: null })
      const result = await decksApi.fetchPublishedDecks()
      expect(result).toEqual([])
    })
  })

  describe('fetchDeckWithQuestions', () => {
    it('returns deck with questions', async () => {
      const deck = { id: 'd1', title: 'Deck', version: 1 }
      const questions = [{ id: 'q1', prompt: 'Q?', sort_order: 0 }]
      nextResults.push({ data: deck, error: null }, { data: questions, error: null })
      const result = await decksApi.fetchDeckWithQuestions('d1')
      expect(result).toMatchObject({ ...deck, questions })
      expect(result.questions).toHaveLength(1)
    })

    it('throws when deck not found', async () => {
      nextResults.push({ data: null, error: new Error('Not found') })
      await expect(decksApi.fetchDeckWithQuestions('bad')).rejects.toThrow()
    })
  })

  describe('userHasDeck', () => {
    it('returns false when userId is null', async () => {
      expect(await decksApi.userHasDeck('d1', null)).toBe(false)
    })

    it('returns true when user has deck', async () => {
      nextResults.push({ data: [{ id: 'd1' }], error: null })
      expect(await decksApi.userHasDeck('d1', 'user1')).toBe(true)
    })

    it('returns false when no match', async () => {
      nextResults.push({ data: [], error: null })
      expect(await decksApi.userHasDeck('d1', 'user1')).toBe(false)
    })
  })

  describe('togglePublished', () => {
    it('calls update and throws on error', async () => {
      nextResults.push({ error: new Error('DB error') })
      await expect(decksApi.togglePublished('d1', true)).rejects.toThrow('DB error')
    })

    it('succeeds when no error', async () => {
      nextResults.push({ error: null })
      await expect(decksApi.togglePublished('d1', false)).resolves.toBeUndefined()
    })
  })

  describe('deleteDeck', () => {
    it('throws on error', async () => {
      nextResults.push({ error: new Error('FK violation') })
      await expect(decksApi.deleteDeck('d1')).rejects.toThrow('FK violation')
    })

    it('succeeds when no error', async () => {
      nextResults.push({ error: null })
      await expect(decksApi.deleteDeck('d1')).resolves.toBeUndefined()
    })
  })

  describe('getMyDeckRating', () => {
    it('returns null when userId is null', async () => {
      expect(await decksApi.getMyDeckRating('d1', null)).toBe(null)
    })

    it('returns rating and comment when present', async () => {
      nextResults.push({ data: { rating: 4, comment: 'Great!' }, error: null })
      const result = await decksApi.getMyDeckRating('d1', 'user1')
      expect(result).toEqual({ rating: 4, comment: 'Great!' })
    })

    it('returns null when no rating', async () => {
      nextResults.push({ data: null, error: null })
      expect(await decksApi.getMyDeckRating('d1', 'user1')).toBe(null)
    })
  })

  describe('submitDeckRating', () => {
    it('clamps rating to 1-5 and trims comment', async () => {
      nextResults.push({ error: null })
      await decksApi.submitDeckRating('d1', 'user1', { rating: 10, comment: '  ok  ' })
      expect(nextResults.length).toBe(0)
    })

    it('throws on error', async () => {
      nextResults.push({ error: new Error('Unique violation') })
      await expect(decksApi.submitDeckRating('d1', 'user1', { rating: 3 })).rejects.toThrow('Unique violation')
    })
  })

  describe('getDeckReviews', () => {
    it('returns empty array when no ratings', async () => {
      nextResults.push({ data: [], error: null })
      const result = await decksApi.getDeckReviews('d1')
      expect(result).toEqual([])
    })

    it('returns reviews with display names from profiles', async () => {
      nextResults.push(
        { data: [{ user_id: 'u1', rating: 5, comment: 'Nice', created_at: '2025-01-01' }], error: null },
        { data: [{ id: 'u1', display_name: 'Alice', email: 'a@b.com', avatar_url: null }], error: null }
      )
      const result = await decksApi.getDeckReviews('d1')
      expect(result).toHaveLength(1)
      expect(result[0]).toMatchObject({ rating: 5, comment: 'Nice', display_name: 'Alice', email: 'a@b.com' })
    })
  })

  describe('createDeck', () => {
    it('creates deck and returns id', async () => {
      nextResults.push({ data: { id: 'new-id' }, error: null })
      const id = await decksApi.createDeck('user1', { title: 'Title', description: 'Desc', config: {} })
      expect(id).toBe('new-id')
    })

    it('throws when insert fails', async () => {
      nextResults.push({ data: null, error: new Error('Failed') })
      await expect(decksApi.createDeck('user1', { title: 'T', description: '', config: {} })).rejects.toThrow()
    })
  })

  describe('updateDeck', () => {
    it('fetches current version then updates', async () => {
      nextResults.push(
        { data: { version: 1, published: false }, error: null },
        { error: null },
        { error: null }
      )
      await decksApi.updateDeck('d1', { title: 'New', description: '', config: {}, questions: [] })
      expect(nextResults.length).toBe(0)
    })

    it('bumps version when published', async () => {
      nextResults.push(
        { data: { version: 2, published: true }, error: null },
        { error: null },
        { error: null }
      )
      await decksApi.updateDeck('d1', { title: 'T', description: '', config: {}, questions: [] })
      expect(nextResults.length).toBe(0)
    })
  })

  describe('copyDeckToUser', () => {
    it('throws when deck not published', async () => {
      nextResults.push(
        { data: { id: 's1', title: 'S', published: false, questions: [] }, error: null },
        { data: [], error: null }
      )
      await expect(decksApi.copyDeckToUser('s1', 'user1')).rejects.toThrow('not available to copy')
    })

    it('creates copy and returns new deck id', async () => {
      nextResults.push(
        { data: { id: 's1', title: 'Source', published: true, version: 1, description: '', config: {}, questions: [] }, error: null },
        { data: [], error: null },
        { data: { id: 'new-id' }, error: null }
      )
      const id = await decksApi.copyDeckToUser('s1', 'user1')
      expect(id).toBe('new-id')
    })
  })

  describe('getSourceUpdatePreview', () => {
    it('throws when deck not found or not a copy', async () => {
      nextResults.push({ data: null, error: new Error('Not found') })
      await expect(decksApi.getSourceUpdatePreview('d1', 'user1')).rejects.toThrow('not found or not a copy')
    })

    it('returns source, copy and changes', async () => {
      const copy = { id: 'c1', copied_from_deck_id: 's1', title: 'Old', description: 'D', questions: [] }
      const sourceDeck = { id: 's1', title: 'New Title', published: true, version: 2, description: 'D', config: {}, questions: [{ prompt: 'Q' }] }
      nextResults.push(
        { data: copy, error: null },
        { data: sourceDeck, error: null },
        { data: [{ prompt: 'Q' }], error: null }
      )
      const result = await decksApi.getSourceUpdatePreview('c1', 'user1')
      expect(result.source).toBeDefined()
      expect(result.copy).toBeDefined()
      expect(result.changes).toEqual(expect.any(Array))
    })
  })

  describe('applySourceUpdate', () => {
    it('throws when deck not a copy', async () => {
      nextResults.push({ data: null, error: new Error('Not found') })
      await expect(decksApi.applySourceUpdate('d1', 'user1')).rejects.toThrow()
    })
  })
})
