import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import { nextTick } from 'vue'
import ChildView from '../ChildView.vue'
import { eventBus } from '@/common/eventBus'
import { helpButtonHidden } from '@/tutorial/controller'

// Mock dependencies
vi.mock('vue-router', () => ({
  useRoute: vi.fn(() => ({
    params: { id: 'child-123' },
  })),
  useRouter: vi.fn(() => ({
    push: vi.fn(),
  })),
}))

global.fetch = vi.fn()

describe('ChildView', () => {
  let wrapper: VueWrapper<any>

  const mockChild = {
    id: 'child-123',
    name: 'Test Child',
    age: 8,
    points: 50,
    tasks: ['task-1', 'task-2'],
    rewards: ['reward-1'],
    image_id: 'boy01',
  }

  const mockChore = {
    id: 'task-1',
    name: 'Clean Room',
    points: 10,
    type: 'chore' as const,
    image_url: '/images/task.png',
    custom_value: null,
  }

  const mockChoreWithOverride = {
    id: 'task-1',
    name: 'Clean Room',
    points: 10,
    type: 'chore' as const,
    image_url: '/images/task.png',
    custom_value: 15,
  }

  const mockPenalty = {
    id: 'task-2',
    name: 'Hit Sibling',
    points: 5,
    type: 'penalty' as const,
    image_url: '/images/penalty.png',
    custom_value: null,
  }

  const mockPenaltyWithOverride = {
    id: 'task-2',
    name: 'Hit Sibling',
    points: 5,
    type: 'penalty' as const,
    image_url: '/images/penalty.png',
    custom_value: 8,
  }

  const mockRoutine = {
    id: 'routine-1',
    name: 'Morning Routine',
    points: 12,
    image_id: 'routine-image',
    image_url: '/images/routine.png',
    items: [
      {
        id: 'routine-item-1',
        routine_id: 'routine-1',
        name: 'Make Bed',
        image_id: null,
        order: 0,
      },
    ],
  }

  beforeEach(() => {
    vi.clearAllMocks()

    // Mock fetch responses
    ;(global.fetch as any).mockImplementation((url: string) => {
      if (url.includes('/child/child-123')) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockChild),
        })
      }
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ tasks: [], reward_status: [] }),
      })
    })

    // Mock speech synthesis
    global.window.speechSynthesis = {
      speak: vi.fn(),
      cancel: vi.fn(),
    } as any
    global.window.SpeechSynthesisUtterance = vi.fn() as any
  })

  afterEach(() => {
    if (wrapper) {
      wrapper.unmount()
    }
  })

  describe('Component Mounting', () => {
    it('loads and displays child data on mount', async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(global.fetch).toHaveBeenCalledWith('/api/child/child-123')
    })

    it('registers SSE event listeners on mount', async () => {
      const onSpy = vi.spyOn(eventBus, 'on')
      wrapper = mount(ChildView)
      await nextTick()

      expect(onSpy).toHaveBeenCalledWith('child_task_triggered', expect.any(Function))
      expect(onSpy).toHaveBeenCalledWith('child_reward_triggered', expect.any(Function))
      expect(onSpy).toHaveBeenCalledWith('child_reward_request', expect.any(Function))
    })

    it('sets up inactivity timer on mount', async () => {
      const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
      wrapper = mount(ChildView)
      await nextTick()

      // Should set up inactivity timer (60 seconds)
      expect(setTimeoutSpy).toHaveBeenCalled()
    })

    it('cleans up inactivity timer on unmount', async () => {
      wrapper = mount(ChildView)
      await nextTick()

      const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
      wrapper.unmount()

      expect(clearTimeoutSpy).toHaveBeenCalled()
    })
  })

  describe('Custom Value Display - Chores', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('displays default points for chore without override', () => {
      // The template should display mockChore.points (10) when custom_value is null
      // Template logic: item.custom_value !== undefined && item.custom_value !== null ? item.custom_value : item.points
      const expectedValue = mockChore.points
      expect(expectedValue).toBe(10)
    })

    it('displays custom_value for chore with override', () => {
      // The template should display mockChoreWithOverride.custom_value (15)
      const expectedValue = mockChoreWithOverride.custom_value
      expect(expectedValue).toBe(15)
    })
  })

  describe('Custom Value Display - Penalties', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('displays negative default points for penalty without override', () => {
      // The template should display -mockPenalty.points (-5)
      // Template logic: item.custom_value !== undefined && item.custom_value !== null ? -item.custom_value : -item.points
      const expectedValue = -mockPenalty.points
      expect(expectedValue).toBe(-5)
    })

    it('displays negative custom_value for penalty with override', () => {
      // The template should display -mockPenaltyWithOverride.custom_value (-8)
      const expectedValue = -mockPenaltyWithOverride.custom_value!
      expect(expectedValue).toBe(-8)
    })
  })

  describe('Task Triggering', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('speaks task name when triggered', () => {
      wrapper.vm.triggerTask(mockChore)

      expect(window.speechSynthesis.cancel).toHaveBeenCalled()
      expect(window.speechSynthesis.speak).toHaveBeenCalled()
    })

    it('does not call trigger-task API in child mode', async () => {
      await wrapper.vm.triggerTask(mockChore)

      expect(
        (global.fetch as any).mock.calls.some((call: [string]) =>
          call[0].includes('/trigger-task'),
        ),
      ).toBe(false)
    })

    it('does not crash if speechSynthesis is not available', () => {
      const originalSpeechSynthesis = global.window.speechSynthesis
      delete (global.window as any).speechSynthesis

      expect(() => wrapper.vm.triggerTask(mockChore)).not.toThrow()

      // Restore for other tests
      global.window.speechSynthesis = originalSpeechSynthesis
    })
  })

  describe('Routine Overlay', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('opens the routine overlay when a routine is clicked', async () => {
      vi.useFakeTimers()

      wrapper.vm.handleRoutineClick(mockRoutine)
      vi.advanceTimersByTime(200)
      await nextTick()

      expect(wrapper.vm.showRoutineOverlay).toBe(true)
      expect(wrapper.vm.dialogRoutine?.id).toBe('routine-1')

      vi.useRealTimers()
    })

    it('speaks the routine task name when a routine task is clicked', () => {
      wrapper.vm.handleRoutineTaskClick(mockRoutine.items[0])

      expect(window.speechSynthesis.cancel).toHaveBeenCalled()
      expect(window.speechSynthesis.speak).toHaveBeenCalled()
    })
  })

  describe('Reward Triggering', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('speaks reward text when triggered', () => {
      wrapper.vm.triggerReward({
        id: 'reward-1',
        name: 'Ice Cream',
        cost: 50,
        points_needed: 10,
        redeeming: false,
      })

      expect(window.speechSynthesis.cancel).toHaveBeenCalled()
      expect(window.speechSynthesis.speak).toHaveBeenCalled()
    })

    it('does not call reward request/cancel APIs in child mode', () => {
      wrapper.vm.triggerReward({
        id: 'reward-1',
        name: 'Ice Cream',
        cost: 50,
        points_needed: 0,
        redeeming: false,
      })

      const requestCalls = (global.fetch as any).mock.calls.filter(
        (call: [string]) =>
          call[0].includes('/request-reward') || call[0].includes('/cancel-request-reward'),
      )
      expect(requestCalls.length).toBe(0)
    })

    it('opens redeem dialog when reward is ready and not pending', async () => {
      vi.useFakeTimers()
      wrapper.vm.triggerReward({
        id: 'reward-1',
        name: 'Ice Cream',
        cost: 50,
        points_needed: 0,
        redeeming: false,
      })
      vi.advanceTimersByTime(200)
      await nextTick()

      expect(wrapper.vm.showRewardDialog).toBe(true)
      expect(wrapper.vm.showCancelDialog).toBe(false)
      expect(wrapper.vm.dialogReward?.id).toBe('reward-1')
      vi.useRealTimers()
    })

    it('does not open redeem dialog when reward is not yet ready', () => {
      wrapper.vm.triggerReward({
        id: 'reward-1',
        name: 'Ice Cream',
        cost: 50,
        points_needed: 10,
        redeeming: false,
      })

      expect(wrapper.vm.showRewardDialog).toBe(false)
      expect(wrapper.vm.showCancelDialog).toBe(false)
    })

    it('opens cancel dialog when reward is already pending', async () => {
      vi.useFakeTimers()
      wrapper.vm.triggerReward({
        id: 'reward-1',
        name: 'Ice Cream',
        cost: 50,
        points_needed: 0,
        redeeming: true,
      })
      vi.advanceTimersByTime(200)
      await nextTick()

      expect(wrapper.vm.showCancelDialog).toBe(true)
      expect(wrapper.vm.showRewardDialog).toBe(false)
      expect(wrapper.vm.dialogReward?.id).toBe('reward-1')
      vi.useRealTimers()
    })
  })

  describe('Reward Redeem Dialog', () => {
    const readyReward = {
      id: 'reward-1',
      name: 'Ice Cream',
      cost: 50,
      points_needed: 0,
      redeeming: false,
    }

    beforeEach(async () => {
      vi.useFakeTimers()
      wrapper = mount(ChildView)
      await nextTick()
      vi.advanceTimersByTime(100)
      await nextTick()
      wrapper.vm.triggerReward(readyReward)
      vi.advanceTimersByTime(200)
      await nextTick()
      vi.useRealTimers()
    })

    it('closes redeem dialog on cancelRedeemReward', async () => {
      expect(wrapper.vm.showRewardDialog).toBe(true)
      wrapper.vm.cancelRedeemReward()
      expect(wrapper.vm.showRewardDialog).toBe(false)
      expect(wrapper.vm.dialogReward).toBe(null)
    })

    it('calls request-reward API on confirmRedeemReward', async () => {
      ;(global.fetch as any).mockResolvedValueOnce({ ok: true })

      await wrapper.vm.confirmRedeemReward()

      expect(global.fetch).toHaveBeenCalledWith(
        `/api/child/child-123/request-reward`,
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ reward_id: 'reward-1' }),
        }),
      )
    })

    it('closes redeem dialog after confirmRedeemReward', async () => {
      ;(global.fetch as any).mockResolvedValueOnce({ ok: true })

      await wrapper.vm.confirmRedeemReward()
      await nextTick()

      expect(wrapper.vm.showRewardDialog).toBe(false)
      expect(wrapper.vm.dialogReward).toBe(null)
    })
  })

  describe('Help FAB visibility during reward redeem dialog', () => {
    beforeEach(() => {
      helpButtonHidden.value = false
      wrapper = mount(ChildView)
    })

    it('hides the help button while the reward redeem dialog is open', async () => {
      wrapper.vm.showRewardDialog = true
      await nextTick()
      expect(helpButtonHidden.value).toBe(true)

      wrapper.vm.showRewardDialog = false
      await nextTick()
      expect(helpButtonHidden.value).toBe(false)
    })
  })

  describe('Cancel Pending Reward Dialog', () => {
    const pendingReward = {
      id: 'reward-1',
      name: 'Ice Cream',
      cost: 50,
      points_needed: 0,
      redeeming: true,
    }

    beforeEach(async () => {
      vi.useFakeTimers()
      wrapper = mount(ChildView)
      await nextTick()
      vi.advanceTimersByTime(100)
      await nextTick()
      wrapper.vm.triggerReward(pendingReward)
      vi.advanceTimersByTime(200)
      await nextTick()
      vi.useRealTimers()
    })

    it('closes cancel dialog on closeCancelDialog', async () => {
      expect(wrapper.vm.showCancelDialog).toBe(true)
      wrapper.vm.closeCancelDialog()
      expect(wrapper.vm.showCancelDialog).toBe(false)
      expect(wrapper.vm.dialogReward).toBe(null)
    })

    it('calls cancel-request-reward API on cancelPendingReward', async () => {
      ;(global.fetch as any).mockResolvedValueOnce({ ok: true })

      await wrapper.vm.cancelPendingReward()

      expect(global.fetch).toHaveBeenCalledWith(
        `/api/child/child-123/cancel-request-reward`,
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ reward_id: 'reward-1' }),
        }),
      )
    })

    it('closes cancel dialog after cancelPendingReward', async () => {
      ;(global.fetch as any).mockResolvedValueOnce({ ok: true })

      await wrapper.vm.cancelPendingReward()
      await nextTick()

      expect(wrapper.vm.showCancelDialog).toBe(false)
      expect(wrapper.vm.dialogReward).toBe(null)
    })
  })

  describe('SSE Event Handlers', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('handles child_task_triggered event and refreshes reward list', async () => {
      const mockRefresh = vi.fn()
      wrapper.vm.childRewardListRef = { refresh: mockRefresh }

      wrapper.vm.handleTaskTriggered({
        type: 'child_task_triggered',
        payload: { child_id: 'child-123', points: 60, task_id: 'task-1' },
      })

      expect(wrapper.vm.child.points).toBe(60)
      expect(mockRefresh).toHaveBeenCalled()
    })

    it('handles child_reward_triggered event', async () => {
      const mockRefresh = vi.fn()
      wrapper.vm.childRewardListRef = { refresh: mockRefresh }

      wrapper.vm.handleRewardTriggered({
        type: 'child_reward_triggered',
        payload: { child_id: 'child-123', points: 40, reward_id: 'reward-1' },
      })

      expect(wrapper.vm.child.points).toBe(40)
      expect(mockRefresh).toHaveBeenCalled()
    })

    it('handles child_tasks_set event', () => {
      wrapper.vm.handleChildTaskSet({
        type: 'child_tasks_set',
        payload: { child_id: 'child-123', task_ids: ['task-1', 'task-3'] },
      })

      expect(wrapper.vm.tasks).toEqual(['task-1', 'task-3'])
    })

    it('handles child_rewards_set event', () => {
      wrapper.vm.handleChildRewardSet({
        type: 'child_rewards_set',
        payload: { child_id: 'child-123', reward_ids: ['reward-1', 'reward-2'] },
      })

      expect(wrapper.vm.rewards).toEqual(['reward-1', 'reward-2'])
    })

    it('handles reward_modified event and refreshes reward list', async () => {
      const mockRefresh = vi.fn()
      wrapper.vm.childRewardListRef = { refresh: mockRefresh }

      wrapper.vm.handleRewardModified({
        type: 'reward_modified',
        payload: { reward_id: 'reward-1', operation: 'EDIT' },
      })

      expect(mockRefresh).toHaveBeenCalled()
    })
  })

  describe('Inactivity Timer', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('resets timer on user interaction', () => {
      const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
      const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')

      wrapper.vm.resetInactivityTimer()

      expect(clearTimeoutSpy).toHaveBeenCalled()
      expect(setTimeoutSpy).toHaveBeenCalled()
    })
  })

  describe('Reward Request Handling', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('handles reward request event and refreshes list', async () => {
      const mockRefresh = vi.fn()
      wrapper.vm.childRewardListRef = { refresh: mockRefresh }

      wrapper.vm.handleRewardRequest({
        type: 'child_reward_request',
        payload: { child_id: 'child-123', reward_id: 'reward-1' },
      })

      expect(mockRefresh).toHaveBeenCalled()
    })

    it('does not refresh if reward not in child rewards', async () => {
      const mockRefresh = vi.fn()
      wrapper.vm.childRewardListRef = { refresh: mockRefresh }

      wrapper.vm.handleRewardRequest({
        type: 'child_reward_request',
        payload: { child_id: 'child-123', reward_id: 'reward-999' },
      })

      expect(mockRefresh).not.toHaveBeenCalled()
    })
  })

  describe('Item Ready State Management', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('initializes readyItemId to null', () => {
      expect(wrapper.vm.readyItemId).toBe(null)
    })

    it('updates readyItemId when handleItemReady is called with an item ID', () => {
      wrapper.vm.handleItemReady('task-1')
      expect(wrapper.vm.readyItemId).toBe('task-1')

      wrapper.vm.handleItemReady('reward-2')
      expect(wrapper.vm.readyItemId).toBe('reward-2')
    })

    it('clears readyItemId when handleItemReady is called with empty string', () => {
      wrapper.vm.readyItemId = 'task-1'
      wrapper.vm.handleItemReady('')
      expect(wrapper.vm.readyItemId).toBe('')
    })

    it('passes readyItemId prop to Chores ScrollingList', async () => {
      wrapper.vm.readyItemId = 'task-1'
      await nextTick()

      const choresScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[0]
      expect(choresScrollingList.props('readyItemId')).toBe('task-1')
    })

    it('passes readyItemId prop to Penalties ScrollingList', async () => {
      wrapper.vm.readyItemId = 'task-2'
      await nextTick()

      const penaltiesScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[1]
      expect(penaltiesScrollingList.props('readyItemId')).toBe('task-2')
    })

    it('passes readyItemId prop to Rewards ScrollingList', async () => {
      wrapper.vm.readyItemId = 'reward-1'
      await nextTick()

      const rewardsScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[2]
      expect(rewardsScrollingList.props('readyItemId')).toBe('reward-1')
    })

    it('handles item-ready event from Chores ScrollingList', async () => {
      const choresScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[0]

      choresScrollingList.vm.$emit('item-ready', 'task-1')
      await nextTick()

      expect(wrapper.vm.readyItemId).toBe('task-1')
    })

    it('handles item-ready event from Penalties ScrollingList', async () => {
      const penaltiesScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[1]

      penaltiesScrollingList.vm.$emit('item-ready', 'task-2')
      await nextTick()

      expect(wrapper.vm.readyItemId).toBe('task-2')
    })

    it('handles item-ready event from Rewards ScrollingList', async () => {
      const rewardsScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[2]

      rewardsScrollingList.vm.$emit('item-ready', 'reward-1')
      await nextTick()

      expect(wrapper.vm.readyItemId).toBe('reward-1')
    })

    it('maintains 2-step click workflow: first click sets ready, second click triggers', async () => {
      // Initial state
      expect(wrapper.vm.readyItemId).toBe(null)

      // First click - item should become ready
      wrapper.vm.handleItemReady('task-1')
      expect(wrapper.vm.readyItemId).toBe('task-1')

      // Second click would trigger the item (tested via ScrollingList component)
      // After trigger, ready state should be cleared
      wrapper.vm.handleItemReady('')
      expect(wrapper.vm.readyItemId).toBe('')
    })
  })

  describe('choreDueLabel', () => {
    beforeEach(async () => {
      wrapper = mount(ChildView)
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('returns null when item has no schedule', () => {
      const result = wrapper.vm.choreDueLabel({
        id: 'task-1',
        name: 'Test',
        points: 5,
        type: 'chore' as const,
        schedule: null,
      })
      expect(result).toBe(null)
    })

    it('returns "Due by ..." prefix format when chore has a future due time today', () => {
      vi.useFakeTimers()
      // Feb 24 2026 is a Tuesday (day index 2) at 10:00am
      vi.setSystemTime(new Date('2026-02-24T10:00:00'))
      try {
        const item = {
          id: 'task-1',
          name: 'Test',
          points: 5,
          type: 'chore' as const,
          schedule: {
            mode: 'days' as const,
            day_configs: [{ day: 2, hour: 14, minute: 30 }], // Tuesday 2:30pm — future
            interval_days: null,
            anchor_weekday: null,
            interval_hour: null,
            interval_minute: null,
          },
          extension_date: null,
        }
        const result = wrapper.vm.choreDueLabel(item)
        expect(result).toMatch(/^Due by /)
      } finally {
        vi.useRealTimers()
      }
    })
  })
})
