// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useJobsStore } from '@/stores/jobs-store'
import useJobsRealtime from '@/hooks/use-jobs-realtime'

type Listener = (event: { data: string }) => void

class MockEventSource {
  static instance: MockEventSource | null = null
  private listeners: Record<string, Listener[]> = {}
  onerror: (() => void) | null = null

  constructor(_url: string) {
    MockEventSource.instance = this
  }

  addEventListener(type: string, listener: Listener) {
    if (!this.listeners[type]) this.listeners[type] = []
    this.listeners[type].push(listener)
  }

  dispatch(type: string, data: unknown) {
    for (const l of this.listeners[type] ?? []) {
      l({ data: JSON.stringify(data) })
    }
  }

  close() {}
}

const fullJob = {
  id: 'job-unknown-1',
  kind: 'TASK_IMPLEMENTATION',
  status: 'RUNNING',
  taskCode: 'T-1',
  taskTitle: 'Test',
  ideaCode: null,
  ideaTitle: null,
  sprintGoal: null,
  sprintCode: null,
  productName: 'Scrum4Me',
  productCode: null,
  storyCode: null,
  pbiCode: null,
  modelId: null,
  inputTokens: null,
  outputTokens: null,
  cacheReadTokens: null,
  cacheWriteTokens: null,
  costUsd: null,
  branch: null,
  prUrl: null,
  error: null,
  summary: null,
  description: null,
  verifyResult: null,
  startedAt: null,
  finishedAt: null,
  createdAt: new Date('2026-01-01'),
  sprintRunId: null,
}

beforeEach(() => {
  vi.stubGlobal('EventSource', MockEventSource)
  MockEventSource.instance = null

  // Lege store
  useJobsStore.setState({ activeJobs: [], doneJobs: [], selectedJobId: null })

  // fetch resolveert naar de volledige job
  vi.stubGlobal(
    'fetch',
    vi.fn().mockImplementation(async () => ({
      ok: true,
      json: async () => fullJob,
    }))
  )
})

afterEach(() => {
  vi.unstubAllGlobals()
  vi.restoreAllMocks()
})

describe('useJobsRealtime: fetch-on-unknown', () => {
  it('haalt onbekende job op via REST bij message-event', async () => {
    renderHook(() => useJobsRealtime())
    const es = MockEventSource.instance!

    // Dispatch twee events met hetzelfde onbekende job_id gelijktijdig
    act(() => {
      es.dispatch('message', { job_id: 'job-unknown-1', status: 'RUNNING' })
      es.dispatch('message', { job_id: 'job-unknown-1', status: 'RUNNING' })
    })

    // Wacht op alle microtasks / fetch-promises
    await act(async () => {
      await Promise.resolve()
    })

    expect(fetch).toHaveBeenCalledTimes(1)
    expect(fetch).toHaveBeenCalledWith('/api/jobs/job-unknown-1')

    const { activeJobs } = useJobsStore.getState()
    expect(activeJobs.some(j => j.id === 'job-unknown-1')).toBe(true)
    expect(activeJobs.find(j => j.id === 'job-unknown-1')?.taskTitle).toBe('Test')
  })

  it('gebruikt partial-upsert voor bekende jobs bij message-event', async () => {
    // Zet een bekende job in de store
    useJobsStore.setState({
      activeJobs: [{ ...fullJob, id: 'job-known-1', status: 'QUEUED' } as never],
      doneJobs: [],
      selectedJobId: null,
    })

    renderHook(() => useJobsRealtime())
    const es = MockEventSource.instance!

    act(() => {
      es.dispatch('message', { job_id: 'job-known-1', status: 'RUNNING', branch: 'feat/x' })
    })

    await act(async () => { await Promise.resolve() })

    expect(fetch).not.toHaveBeenCalled()
    const { activeJobs } = useJobsStore.getState()
    expect(activeJobs.find(j => j.id === 'job-known-1')?.status).toBe('RUNNING')
  })

  it('haalt onbekende job op via REST bij jobs_initial-event', async () => {
    renderHook(() => useJobsRealtime())
    const es = MockEventSource.instance!

    act(() => {
      es.dispatch('jobs_initial', [{ job_id: 'job-unknown-1', status: 'RUNNING' }])
    })

    await act(async () => { await Promise.resolve() })

    expect(fetch).toHaveBeenCalledTimes(1)
    expect(fetch).toHaveBeenCalledWith('/api/jobs/job-unknown-1')
    const { activeJobs } = useJobsStore.getState()
    expect(activeJobs.some(j => j.id === 'job-unknown-1')).toBe(true)
  })
})
