/**
 * Unit tests for useLoraCyclerState composable
 *
 * Tests pure state transitions and index calculations in isolation.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useLoraCyclerState } from '@/composables/useLoraCyclerState'
import {
  createMockWidget,
  createMockCyclerConfig,
  createMockPoolConfig
} from '../fixtures/mockConfigs'
import { setupFetchMock, resetFetchMock } from '../setup'

describe('useLoraCyclerState', () => {
  beforeEach(() => {
    resetFetchMock()
  })

  describe('Initial State', () => {
    it('should initialize with default values', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      expect(state.currentIndex.value).toBe(1)
      expect(state.totalCount.value).toBe(0)
      expect(state.poolConfigHash.value).toBe('')
      expect(state.modelStrength.value).toBe(1.0)
      expect(state.clipStrength.value).toBe(1.0)
      expect(state.useCustomClipRange.value).toBe(false)
      expect(state.sortBy.value).toBe('filename')
      expect(state.executionIndex.value).toBeNull()
      expect(state.nextIndex.value).toBeNull()
      expect(state.repeatCount.value).toBe(1)
      expect(state.repeatUsed.value).toBe(0)
      expect(state.displayRepeatUsed.value).toBe(0)
      expect(state.isPaused.value).toBe(false)
    })
  })

  describe('restoreFromConfig', () => {
    it('should restore state from config object', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const config = createMockCyclerConfig({
        current_index: 3,
        total_count: 10,
        model_strength: 0.8,
        clip_strength: 0.6,
        use_same_clip_strength: false,
        repeat_count: 2,
        repeat_used: 1,
        is_paused: true
      })

      state.restoreFromConfig(config)

      expect(state.currentIndex.value).toBe(3)
      expect(state.totalCount.value).toBe(10)
      expect(state.modelStrength.value).toBe(0.8)
      expect(state.clipStrength.value).toBe(0.6)
      expect(state.useCustomClipRange.value).toBe(true) // inverted from use_same_clip_strength
      expect(state.repeatCount.value).toBe(2)
      expect(state.repeatUsed.value).toBe(1)
      expect(state.isPaused.value).toBe(true)
    })

    it('should handle missing optional fields with defaults', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      // Minimal config
      state.restoreFromConfig({
        current_index: 5,
        total_count: 10,
        pool_config_hash: '',
        model_strength: 1.0,
        clip_strength: 1.0,
        use_same_clip_strength: true,
        use_preset_strength: false,
        preset_strength_scale: 1.0,
        sort_by: 'filename',
        current_lora_name: '',
        current_lora_filename: '',
        repeat_count: 1,
        repeat_used: 0,
        is_paused: false,
        include_no_lora: false
      })

      expect(state.currentIndex.value).toBe(5)
      expect(state.repeatCount.value).toBe(1)
      expect(state.isPaused.value).toBe(false)
    })

    it('should not restore execution_index and next_index (transient values)', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      // Set execution indices
      state.executionIndex.value = 2
      state.nextIndex.value = 3

      // Restore from config (these fields in config should be ignored)
      state.restoreFromConfig(createMockCyclerConfig({
        execution_index: 5,
        next_index: 6
      }))

      // Execution indices should remain unchanged
      expect(state.executionIndex.value).toBe(2)
      expect(state.nextIndex.value).toBe(3)
    })
  })

  describe('buildConfig', () => {
    it('should build config object from current state', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.currentIndex.value = 3
      state.totalCount.value = 10
      state.modelStrength.value = 0.8
      state.repeatCount.value = 2
      state.repeatUsed.value = 1
      state.isPaused.value = true

      const config = state.buildConfig()

      expect(config.current_index).toBe(3)
      expect(config.total_count).toBe(10)
      expect(config.model_strength).toBe(0.8)
      expect(config.repeat_count).toBe(2)
      expect(config.repeat_used).toBe(1)
      expect(config.is_paused).toBe(true)
    })
  })

  describe('setIndex', () => {
    it('should set index within valid range', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 10

      state.setIndex(5)
      expect(state.currentIndex.value).toBe(5)

      state.setIndex(1)
      expect(state.currentIndex.value).toBe(1)

      state.setIndex(10)
      expect(state.currentIndex.value).toBe(10)
    })

    it('should not set index outside valid range', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 10
      state.currentIndex.value = 5

      state.setIndex(0)
      expect(state.currentIndex.value).toBe(5) // unchanged

      state.setIndex(11)
      expect(state.currentIndex.value).toBe(5) // unchanged

      state.setIndex(-1)
      expect(state.currentIndex.value).toBe(5) // unchanged
    })
  })

  describe('resetIndex', () => {
    it('should reset index to 1 and clear repeatUsed and displayRepeatUsed', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.currentIndex.value = 5
      state.repeatUsed.value = 2
      state.displayRepeatUsed.value = 2
      state.isPaused.value = true

      state.resetIndex()

      expect(state.currentIndex.value).toBe(1)
      expect(state.repeatUsed.value).toBe(0)
      expect(state.displayRepeatUsed.value).toBe(0)
      expect(state.isPaused.value).toBe(true) // isPaused should NOT be reset
    })
  })

  describe('togglePause', () => {
    it('should toggle pause state', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      expect(state.isPaused.value).toBe(false)

      state.togglePause()
      expect(state.isPaused.value).toBe(true)

      state.togglePause()
      expect(state.isPaused.value).toBe(false)
    })
  })

  describe('generateNextIndex', () => {
    it('should shift indices correctly', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.currentIndex.value = 1
      state.nextIndex.value = 2

      // First call: executionIndex becomes 2 (previous nextIndex), nextIndex becomes 3
      state.generateNextIndex()

      expect(state.executionIndex.value).toBe(2)
      expect(state.nextIndex.value).toBe(3)

      // Second call: executionIndex becomes 3, nextIndex becomes 4
      state.generateNextIndex()

      expect(state.executionIndex.value).toBe(3)
      expect(state.nextIndex.value).toBe(4)
    })

    it('should wrap index from totalCount to 1', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.nextIndex.value = 5 // At the last index

      state.generateNextIndex()

      expect(state.executionIndex.value).toBe(5)
      expect(state.nextIndex.value).toBe(1) // Wrapped to 1
    })

    it('should use currentIndex when nextIndex is null', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.currentIndex.value = 3
      state.nextIndex.value = null

      state.generateNextIndex()

      // executionIndex becomes previous nextIndex (null)
      expect(state.executionIndex.value).toBeNull()
      // nextIndex is calculated from currentIndex (3) -> 4
      expect(state.nextIndex.value).toBe(4)
    })
  })

  describe('initializeNextIndex', () => {
    it('should initialize nextIndex to currentIndex + 1 when null', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.currentIndex.value = 1
      state.nextIndex.value = null

      state.initializeNextIndex()

      expect(state.nextIndex.value).toBe(2)
    })

    it('should wrap nextIndex when currentIndex is at totalCount', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.currentIndex.value = 5
      state.nextIndex.value = null

      state.initializeNextIndex()

      expect(state.nextIndex.value).toBe(1) // Wrapped
    })

    it('should not change nextIndex if already set', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 5
      state.currentIndex.value = 1
      state.nextIndex.value = 4

      state.initializeNextIndex()

      expect(state.nextIndex.value).toBe(4) // Unchanged
    })
  })

  describe('Index Wrapping Edge Cases', () => {
    it('should handle single item pool', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 1
      state.currentIndex.value = 1
      state.nextIndex.value = null

      state.initializeNextIndex()

      expect(state.nextIndex.value).toBe(1) // Wraps back to 1
    })

    it('should handle zero total count gracefully', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.totalCount.value = 0
      state.currentIndex.value = 1
      state.nextIndex.value = null

      state.initializeNextIndex()

      // Should still calculate, even if totalCount is 0
      expect(state.nextIndex.value).toBe(2) // No wrapping since totalCount <= 0
    })
  })

  describe('hashPoolConfig', () => {
    it('should generate consistent hash for same config', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const config1 = createMockPoolConfig()
      const config2 = createMockPoolConfig()

      const hash1 = state.hashPoolConfig(config1)
      const hash2 = state.hashPoolConfig(config2)

      expect(hash1).toBe(hash2)
    })

    it('should generate different hash for different configs', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const config1 = createMockPoolConfig({
        filters: {
          baseModels: ['SD 1.5'],
          tags: { include: [], exclude: [] },
          folders: { include: [], exclude: [] },
          license: { noCreditRequired: false, allowSelling: false },
          namePatterns: { include: [], exclude: [], useRegex: false }
        }
      })

      const config2 = createMockPoolConfig({
        filters: {
          baseModels: ['SDXL'],
          tags: { include: [], exclude: [] },
          folders: { include: [], exclude: [] },
          license: { noCreditRequired: false, allowSelling: false },
          namePatterns: { include: [], exclude: [], useRegex: false }
        }
      })

      const hash1 = state.hashPoolConfig(config1)
      const hash2 = state.hashPoolConfig(config2)

      expect(hash1).not.toBe(hash2)
    })

    it('should return empty string for null config', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      expect(state.hashPoolConfig(null)).toBe('')
    })

    it('should return empty string for config without filters', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const config = { version: 1, preview: { matchCount: 0, lastUpdated: 0 } } as any

      expect(state.hashPoolConfig(config)).toBe('')
    })
  })

  describe('Clip Strength Synchronization', () => {
    it('should sync clipStrength with modelStrength when useCustomClipRange is false', async () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.useCustomClipRange.value = false
      state.modelStrength.value = 0.5

      // Wait for Vue reactivity
      await vi.waitFor(() => {
        expect(state.clipStrength.value).toBe(0.5)
      })
    })

    it('should not sync clipStrength when useCustomClipRange is true', async () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.useCustomClipRange.value = true
      state.clipStrength.value = 0.7
      state.modelStrength.value = 0.5

      // clipStrength should remain unchanged
      await vi.waitFor(() => {
        expect(state.clipStrength.value).toBe(0.7)
      })
    })
  })

  describe('Widget Value Synchronization', () => {
    it('should update widget.value when state changes', async () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.currentIndex.value = 3
      state.repeatCount.value = 2

      // Wait for Vue reactivity
      await vi.waitFor(() => {
        expect(widget.value?.current_index).toBe(3)
        expect(widget.value?.repeat_count).toBe(2)
      })
    })
  })

  describe('Repeat Logic State', () => {
    it('should track repeatUsed correctly', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.repeatCount.value = 3
      expect(state.repeatUsed.value).toBe(0)

      state.repeatUsed.value = 1
      expect(state.repeatUsed.value).toBe(1)

      state.repeatUsed.value = 3
      expect(state.repeatUsed.value).toBe(3)
    })
  })

  describe('fetchCyclerList', () => {
    it('should call API and return lora list', async () => {
      const mockLoras = [
        { file_name: 'lora1.safetensors', model_name: 'LoRA 1' },
        { file_name: 'lora2.safetensors', model_name: 'LoRA 2' }
      ]

      setupFetchMock({ success: true, loras: mockLoras })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const result = await state.fetchCyclerList(null)

      expect(result).toEqual(mockLoras)
      expect(state.isLoading.value).toBe(false)
    })

    it('should include pool config filters in request', async () => {
      const mockFetch = setupFetchMock({ success: true, loras: [] })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const poolConfig = createMockPoolConfig()
      await state.fetchCyclerList(poolConfig)

      expect(mockFetch).toHaveBeenCalledWith(
        '/api/lm/loras/cycler-list',
        expect.objectContaining({
          method: 'POST',
          body: expect.stringContaining('pool_config')
        })
      )
    })

    it('should set isLoading during fetch', async () => {
      let resolvePromise: (value: unknown) => void
      const pendingPromise = new Promise(resolve => {
        resolvePromise = resolve
      })

      // Use mockFetch from setup instead of overriding global
      const { mockFetch } = await import('../setup')
      mockFetch.mockReset()
      mockFetch.mockReturnValue(pendingPromise)

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      const fetchPromise = state.fetchCyclerList(null)

      expect(state.isLoading.value).toBe(true)

      // Resolve the fetch
      resolvePromise!({
        ok: true,
        json: () => Promise.resolve({ success: true, loras: [] })
      })

      await fetchPromise

      expect(state.isLoading.value).toBe(false)
    })
  })

  describe('refreshList', () => {
    it('should update totalCount from API response', async () => {
      const mockLoras = [
        { file_name: 'lora1.safetensors', model_name: 'LoRA 1' },
        { file_name: 'lora2.safetensors', model_name: 'LoRA 2' },
        { file_name: 'lora3.safetensors', model_name: 'LoRA 3' }
      ]

      // Reset and setup fresh mock
      resetFetchMock()
      setupFetchMock({ success: true, loras: mockLoras })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      await state.refreshList(null)

      expect(state.totalCount.value).toBe(3)
    })

    it('should reset index to 1 when pool config hash changes', async () => {
      resetFetchMock()
      setupFetchMock({ success: true, loras: [{ file_name: 'lora1.safetensors', model_name: 'LoRA 1' }] })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      // Set initial state
      state.currentIndex.value = 5
      state.poolConfigHash.value = 'old-hash'

      // Refresh with new config (different hash)
      const newConfig = createMockPoolConfig({
        filters: {
          baseModels: ['SDXL'],
          tags: { include: [], exclude: [] },
          folders: { include: [], exclude: [] },
          license: { noCreditRequired: false, allowSelling: false },
          namePatterns: { include: [], exclude: [], useRegex: false }
        }
      })

      await state.refreshList(newConfig)

      expect(state.currentIndex.value).toBe(1)
    })

    it('should clamp index when totalCount decreases', async () => {
      // Setup mock first, then create state
      resetFetchMock()
      setupFetchMock({
        success: true,
        loras: [
          { file_name: 'lora1.safetensors', model_name: 'LoRA 1' },
          { file_name: 'lora2.safetensors', model_name: 'LoRA 2' },
          { file_name: 'lora3.safetensors', model_name: 'LoRA 3' }
        ]
      })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      // Set initial state with high index
      state.currentIndex.value = 10
      state.totalCount.value = 10

      await state.refreshList(null)

      expect(state.totalCount.value).toBe(3)
      expect(state.currentIndex.value).toBe(3) // Clamped to max
    })

    it('should update currentLoraName and currentLoraFilename', async () => {
      resetFetchMock()
      setupFetchMock({
        success: true,
        loras: [
          { file_name: 'lora1.safetensors', model_name: 'LoRA 1' },
          { file_name: 'lora2.safetensors', model_name: 'LoRA 2' }
        ]
      })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      // Set totalCount first so setIndex works, then set index
      state.totalCount.value = 2
      state.currentIndex.value = 2

      await state.refreshList(null)

      expect(state.currentLoraFilename.value).toBe('lora2.safetensors')
    })

    it('should handle empty list gracefully', async () => {
      resetFetchMock()
      setupFetchMock({ success: true, loras: [] })

      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)
      state.currentIndex.value = 5
      state.totalCount.value = 5

      await state.refreshList(null)

      expect(state.totalCount.value).toBe(0)
      // When totalCount is 0, Math.max(1, 0) = 1, but if currentIndex > totalCount it gets clamped to max(1, totalCount)
      // Looking at the actual code: Math.max(1, totalCount) where totalCount=0 gives 1
      expect(state.currentIndex.value).toBe(1)
      expect(state.currentLoraName.value).toBe('')
      expect(state.currentLoraFilename.value).toBe('')
    })
  })

  describe('isClipStrengthDisabled computed', () => {
    it('should return true when useCustomClipRange is false', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.useCustomClipRange.value = false
      expect(state.isClipStrengthDisabled.value).toBe(true)
    })

    it('should return false when useCustomClipRange is true', () => {
      const widget = createMockWidget()
      const state = useLoraCyclerState(widget)

      state.useCustomClipRange.value = true
      expect(state.isClipStrengthDisabled.value).toBe(false)
    })
  })
})
