import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import { useSprintWorkspaceStore } from '@/stores/sprint-workspace/store'
import type {
  SprintWorkspaceSnapshot,
  SprintWorkspaceSprint,
  SprintWorkspaceStory,
  SprintWorkspaceTask,
  SprintWorkspaceTaskDetail,
} from '@/stores/sprint-workspace/types'

// G5: snapshot original actions on module-load; restore in beforeEach.
const originalActions = (() => {
  const s = useSprintWorkspaceStore.getState()
  return {
    hydrateSnapshot: s.hydrateSnapshot,
    hydrateProductSprints: s.hydrateProductSprints,
    setActiveProduct: s.setActiveProduct,
    setActiveSprint: s.setActiveSprint,
    setActiveStory: s.setActiveStory,
    setActiveTask: s.setActiveTask,
    ensureProductSprintsLoaded: s.ensureProductSprintsLoaded,
    ensureSprintLoaded: s.ensureSprintLoaded,
    ensureStoryLoaded: s.ensureStoryLoaded,
    ensureTaskLoaded: s.ensureTaskLoaded,
    applyRealtimeEvent: s.applyRealtimeEvent,
    resyncActiveScopes: s.resyncActiveScopes,
    resyncLoadedScopes: s.resyncLoadedScopes,
    applyOptimisticMutation: s.applyOptimisticMutation,
    rollbackMutation: s.rollbackMutation,
    settleMutation: s.settleMutation,
    setRealtimeStatus: s.setRealtimeStatus,
  }
})()

function resetStore() {
  useSprintWorkspaceStore.setState((s) => {
    s.context.activeProduct = null
    s.context.activeSprintId = null
    s.context.activeStoryId = null
    s.context.activeTaskId = null
    s.entities.sprintsById = {}
    s.entities.storiesById = {}
    s.entities.tasksById = {}
    s.relations.sprintIdsByProduct = {}
    s.relations.storyIdsBySprint = {}
    s.relations.taskIdsByStory = {}
    s.loading.loadedProductSprintsIds = {}
    s.loading.loadingProductId = null
    s.loading.loadedSprintIds = {}
    s.loading.loadingSprintId = null
    s.loading.loadedStoryIds = {}
    s.loading.loadedTaskIds = {}
    s.loading.activeRequestId = null
    s.sync.realtimeStatus = 'connecting'
    s.sync.lastEventAt = null
    s.sync.lastResyncAt = null
    s.sync.resyncReason = null
    s.pendingMutations = {}
    Object.assign(s, originalActions)
  })
}

beforeEach(() => {
  resetStore()
})

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

function makeSprint(
  overrides: Partial<SprintWorkspaceSprint> & { id: string; product_id: string },
): SprintWorkspaceSprint {
  return {
    id: overrides.id,
    product_id: overrides.product_id,
    code: overrides.code ?? `S-${overrides.id}`,
    sprint_goal: overrides.sprint_goal ?? `Goal ${overrides.id}`,
    status: overrides.status ?? 'OPEN',
    start_date: overrides.start_date ?? '2026-04-01',
    end_date: overrides.end_date ?? '2026-04-14',
    created_at: overrides.created_at ?? new Date('2026-03-15'),
    completed_at: overrides.completed_at ?? null,
  }
}

function makeStory(
  overrides: Partial<SprintWorkspaceStory> & { id: string; pbi_id: string },
): SprintWorkspaceStory {
  return {
    id: overrides.id,
    code: overrides.code ?? overrides.id,
    title: overrides.title ?? `Story ${overrides.id}`,
    description: overrides.description ?? null,
    acceptance_criteria: overrides.acceptance_criteria ?? null,
    priority: overrides.priority ?? 2,
    sort_order: overrides.sort_order ?? 1,
    status: overrides.status ?? 'OPEN',
    pbi_id: overrides.pbi_id,
    sprint_id: overrides.sprint_id ?? null,
    created_at: overrides.created_at ?? new Date('2026-01-01'),
  }
}

function makeTask(
  overrides: Partial<SprintWorkspaceTask> & { id: string; story_id: string },
): SprintWorkspaceTask {
  return {
    id: overrides.id,
    code: overrides.code ?? null,
    title: overrides.title ?? `Task ${overrides.id}`,
    description: overrides.description ?? null,
    priority: overrides.priority ?? 2,
    sort_order: overrides.sort_order ?? 1,
    status: overrides.status ?? 'TO_DO',
    story_id: overrides.story_id,
    sprint_id: overrides.sprint_id ?? null,
    created_at: overrides.created_at ?? new Date('2026-01-01'),
  }
}

function snapshotWith(
  sprint: SprintWorkspaceSprint | undefined,
  stories: SprintWorkspaceStory[] = [],
  tasksByStory: Record<string, SprintWorkspaceTask[]> = {},
  product?: { id: string; name: string },
): SprintWorkspaceSnapshot {
  return { product, sprint, stories, tasksByStory }
}

// G7/G8: mock fetch via mockImplementation (vers Response per call)
function mockFetchSequence(
  responses: Array<unknown | ((url: string, init?: RequestInit) => unknown)>,
) {
  let i = 0
  return vi.spyOn(globalThis, 'fetch').mockImplementation((async (url: string, init?: RequestInit) => {
    const r = responses[Math.min(i, responses.length - 1)]
    i += 1
    const body = typeof r === 'function' ? (r as (u: string, i?: RequestInit) => unknown)(url, init) : r
    return new Response(JSON.stringify(body ?? null), { status: 200 })
  }) as unknown as typeof fetch)
}

// ─────────────────────────────────────────────────────────────────────────
// hydrateSnapshot
// ─────────────────────────────────────────────────────────────────────────

describe('hydrateSnapshot', () => {
  it('vult entities, relations en loaded-marker', () => {
    const sprint = makeSprint({ id: 'sp-1', product_id: 'prod-1' })
    const storyA = makeStory({ id: 's-a', pbi_id: 'pbi-1', sprint_id: 'sp-1', sort_order: 2 })
    const storyB = makeStory({ id: 's-b', pbi_id: 'pbi-1', sprint_id: 'sp-1', sort_order: 1 })
    const taskA = makeTask({ id: 't-a', story_id: 's-a', sort_order: 2 })
    const taskB = makeTask({ id: 't-b', story_id: 's-a', sort_order: 1 })

    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        sprint,
        [storyA, storyB],
        { 's-a': [taskA, taskB] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )

    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.sprintsById['sp-1']).toEqual(sprint)
    expect(s.entities.storiesById['s-a']).toEqual(storyA)
    expect(s.entities.storiesById['s-b']).toEqual(storyB)
    // sort_order: storyB (1) before storyA (2)
    expect(s.relations.storyIdsBySprint['sp-1']).toEqual(['s-b', 's-a'])
    // sort_order: taskB (1) before taskA (2)
    expect(s.relations.taskIdsByStory['s-a']).toEqual(['t-b', 't-a'])
    expect(s.context.activeProduct).toEqual({ id: 'prod-1', name: 'Product 1' })
    expect(s.loading.loadedSprintIds['sp-1']).toBe(true)
  })

  it('normaliseert API-statussen naar het interne store-contract', () => {
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        [makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1', status: 'in_sprint' })],
        { 's-1': [makeTask({ id: 't-1', story_id: 's-1', status: 'todo' })] },
      ),
    )

    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.storiesById['s-1'].status).toBe('IN_SPRINT')
    expect(s.entities.tasksById['t-1'].status).toBe('TO_DO')
  })
})

describe('hydrateProductSprints', () => {
  it('sorteert OPEN voor CLOSED, dan op start_date desc', () => {
    const closedOld = makeSprint({
      id: 'sp-closed-old',
      product_id: 'prod-1',
      status: 'CLOSED',
      start_date: '2026-01-01',
    })
    const openNew = makeSprint({
      id: 'sp-open-new',
      product_id: 'prod-1',
      status: 'OPEN',
      start_date: '2026-04-01',
    })
    const openOld = makeSprint({
      id: 'sp-open-old',
      product_id: 'prod-1',
      status: 'OPEN',
      start_date: '2026-02-01',
    })

    useSprintWorkspaceStore
      .getState()
      .hydrateProductSprints('prod-1', [closedOld, openOld, openNew])

    const s = useSprintWorkspaceStore.getState()
    expect(s.relations.sprintIdsByProduct['prod-1']).toEqual([
      'sp-open-new',
      'sp-open-old',
      'sp-closed-old',
    ])
    expect(s.loading.loadedProductSprintsIds['prod-1']).toBe(true)
  })
})

// ─────────────────────────────────────────────────────────────────────────
// Selection cascade
// ─────────────────────────────────────────────────────────────────────────

describe('selection cascade', () => {
  it('setActiveSprint reset story+task; setActiveStory reset task', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeSprintId = 'sp-old'
      s.context.activeStoryId = 's-old'
      s.context.activeTaskId = 't-old'
    })

    useSprintWorkspaceStore.getState().setActiveSprint('sp-new')
    let s = useSprintWorkspaceStore.getState()
    expect(s.context.activeSprintId).toBe('sp-new')
    expect(s.context.activeStoryId).toBeNull()
    expect(s.context.activeTaskId).toBeNull()

    useSprintWorkspaceStore.setState((draft) => {
      draft.context.activeStoryId = 's-old'
      draft.context.activeTaskId = 't-old'
    })
    useSprintWorkspaceStore.getState().setActiveStory('s-new')
    s = useSprintWorkspaceStore.getState()
    expect(s.context.activeStoryId).toBe('s-new')
    expect(s.context.activeTaskId).toBeNull()
  })

  it('setActiveProduct(null) ruimt entities en relations op', () => {
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        [makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1' })],
        { 's-1': [makeTask({ id: 't-1', story_id: 's-1' })] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )

    useSprintWorkspaceStore.getState().setActiveProduct(null)
    const s = useSprintWorkspaceStore.getState()
    expect(s.context.activeProduct).toBeNull()
    expect(s.context.activeSprintId).toBeNull()
    expect(s.entities.sprintsById).toEqual({})
    expect(s.entities.storiesById).toEqual({})
    expect(s.entities.tasksById).toEqual({})
    expect(s.relations.sprintIdsByProduct).toEqual({})
    expect(s.relations.storyIdsBySprint).toEqual({})
    expect(s.relations.taskIdsByStory).toEqual({})
  })
})

// ─────────────────────────────────────────────────────────────────────────
// applyRealtimeEvent
// ─────────────────────────────────────────────────────────────────────────

describe('applyRealtimeEvent — sprint', () => {
  beforeEach(() => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
  })

  it('I — voegt sprint toe aan product-lijst', () => {
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'I',
      id: 'sp-new',
      product_id: 'prod-1',
      code: 'S-1',
      sprint_goal: 'New Sprint',
      status: 'OPEN',
      start_date: '2026-05-01',
      end_date: '2026-05-14',
      created_at: new Date('2026-04-15').toISOString(),
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.sprintsById['sp-new']).toBeDefined()
    expect(s.relations.sprintIdsByProduct['prod-1']).toContain('sp-new')
  })

  it('I — idempotent voor bestaande id', () => {
    useSprintWorkspaceStore
      .getState()
      .hydrateProductSprints('prod-1', [
        makeSprint({ id: 'sp-1', product_id: 'prod-1', sprint_goal: 'Origineel' }),
      ])
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'I',
      id: 'sp-1',
      product_id: 'prod-1',
      sprint_goal: 'echo',
    })
    expect(useSprintWorkspaceStore.getState().entities.sprintsById['sp-1'].sprint_goal).toBe(
      'Origineel',
    )
  })

  it('U — patch + her-sorteert', () => {
    useSprintWorkspaceStore
      .getState()
      .hydrateProductSprints('prod-1', [
        makeSprint({
          id: 'sp-a',
          product_id: 'prod-1',
          status: 'OPEN',
          start_date: '2026-04-01',
        }),
        makeSprint({
          id: 'sp-b',
          product_id: 'prod-1',
          status: 'OPEN',
          start_date: '2026-03-01',
        }),
      ])
    // sp-a (newer) komt eerst
    expect(
      useSprintWorkspaceStore.getState().relations.sprintIdsByProduct['prod-1'],
    ).toEqual(['sp-a', 'sp-b'])

    // Sluit sp-a → moet naar achteren
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'U',
      id: 'sp-a',
      product_id: 'prod-1',
      status: 'CLOSED',
    })
    expect(
      useSprintWorkspaceStore.getState().relations.sprintIdsByProduct['prod-1'],
    ).toEqual(['sp-b', 'sp-a'])
  })

  it('D — verwijdert sprint inclusief child stories en tasks', () => {
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        [makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1' })],
        { 's-1': [makeTask({ id: 't-1', story_id: 's-1' })] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'D',
      id: 'sp-1',
      product_id: 'prod-1',
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.sprintsById['sp-1']).toBeUndefined()
    expect(s.entities.storiesById['s-1']).toBeUndefined()
    expect(s.entities.tasksById['t-1']).toBeUndefined()
    expect(s.relations.storyIdsBySprint['sp-1']).toBeUndefined()
    expect(s.relations.taskIdsByStory['s-1']).toBeUndefined()
  })

  it('D — clear actieve sprint selectie als die de verwijderde sprint was', () => {
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(makeSprint({ id: 'sp-1', product_id: 'prod-1' }), []),
    )
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeSprintId = 'sp-1'
      s.context.activeStoryId = 's-x'
      s.context.activeTaskId = 't-x'
    })
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'D',
      id: 'sp-1',
      product_id: 'prod-1',
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.context.activeSprintId).toBeNull()
    expect(s.context.activeStoryId).toBeNull()
    expect(s.context.activeTaskId).toBeNull()
  })
})

describe('applyRealtimeEvent — story sprint-move', () => {
  beforeEach(() => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        [makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1' })],
      ),
    )
  })

  it('U met andere sprint_id verplaatst story uit sprint-relatie', () => {
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'story',
      op: 'U',
      id: 's-1',
      product_id: 'prod-1',
      pbi_id: 'pbi-1',
      sprint_id: 'sp-other',
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.relations.storyIdsBySprint['sp-1']).toEqual([])
    expect(s.relations.storyIdsBySprint['sp-other']).toEqual(['s-1'])
    expect(s.entities.storiesById['s-1'].sprint_id).toBe('sp-other')
  })

  it('U met sprint_id=null haalt story uit sprint-relatie', () => {
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'story',
      op: 'U',
      id: 's-1',
      product_id: 'prod-1',
      pbi_id: 'pbi-1',
      sprint_id: null,
    })
    expect(useSprintWorkspaceStore.getState().relations.storyIdsBySprint['sp-1']).toEqual([])
    expect(useSprintWorkspaceStore.getState().entities.storiesById['s-1'].sprint_id).toBeNull()
  })
})

describe('applyRealtimeEvent — task parent-move', () => {
  beforeEach(() => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useSprintWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        [
          makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1' }),
          makeStory({ id: 's-2', pbi_id: 'pbi-1', sprint_id: 'sp-1' }),
        ],
        {
          's-1': [makeTask({ id: 't-1', story_id: 's-1' })],
          's-2': [],
        },
      ),
    )
  })

  it('U met andere story_id verplaatst task naar nieuwe parent', () => {
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'task',
      op: 'U',
      id: 't-1',
      product_id: 'prod-1',
      story_id: 's-2',
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.relations.taskIdsByStory['s-1']).toEqual([])
    expect(s.relations.taskIdsByStory['s-2']).toEqual(['t-1'])
    expect(s.entities.tasksById['t-1'].story_id).toBe('s-2')
  })
})

describe('applyRealtimeEvent — andere product genegeerd', () => {
  it('event met ander product_id raakt de store niet', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useSprintWorkspaceStore
      .getState()
      .hydrateProductSprints('prod-1', [makeSprint({ id: 'sp-1', product_id: 'prod-1' })])
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'I',
      id: 'sp-other',
      product_id: 'prod-2',
      code: 'X',
    })
    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.sprintsById['sp-other']).toBeUndefined()
  })
})

describe('applyRealtimeEvent — unknown entity → resync trigger', () => {
  function withSpy(): ReturnType<typeof vi.fn> {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    const spy = vi.fn().mockResolvedValue(undefined)
    useSprintWorkspaceStore.setState((s) => {
      s.resyncActiveScopes = spy as unknown as typeof s.resyncActiveScopes
    })
    return spy
  }

  it('unknown entity met matching product triggert resync', () => {
    const spy = withSpy()
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'comment',
      op: 'I',
      id: 'cm-1',
      product_id: 'prod-1',
    })
    expect(spy).toHaveBeenCalledWith('unknown-event')
  })

  it('unknown entity met ander product_id triggert geen resync', () => {
    const spy = withSpy()
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'comment',
      op: 'I',
      id: 'cm-1',
      product_id: 'prod-2',
    })
    expect(spy).not.toHaveBeenCalled()
  })

  it('claude_job_status (type-veld) triggert geen resync', () => {
    const spy = withSpy()
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      type: 'claude_job_status',
      job_id: 'job-1',
      product_id: 'prod-1',
      status: 'queued',
    })
    expect(spy).not.toHaveBeenCalled()
  })

  it('worker_heartbeat (type-veld) triggert geen resync', () => {
    const spy = withSpy()
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      type: 'worker_heartbeat',
      worker_id: 'w-1',
      product_id: 'prod-1',
    })
    expect(spy).not.toHaveBeenCalled()
  })

  it('payload zonder entity en zonder type wordt genegeerd', () => {
    const spy = withSpy()
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      product_id: 'prod-1',
      something: 'else',
    })
    expect(spy).not.toHaveBeenCalled()
  })
})

// ─────────────────────────────────────────────────────────────────────────
// ensure*Loaded
// ─────────────────────────────────────────────────────────────────────────

describe('ensureProductSprintsLoaded', () => {
  it('fetcht sprint-list en hydreert met sortering', async () => {
    const sprints = [
      makeSprint({
        id: 'sp-old',
        product_id: 'prod-1',
        status: 'CLOSED',
        start_date: '2026-01-01',
      }),
      makeSprint({
        id: 'sp-new',
        product_id: 'prod-1',
        status: 'OPEN',
        start_date: '2026-04-01',
      }),
    ]
    const fetchSpy = mockFetchSequence([sprints])

    await useSprintWorkspaceStore.getState().ensureProductSprintsLoaded('prod-1')

    expect(fetchSpy).toHaveBeenCalledWith(
      '/api/products/prod-1/sprints',
      expect.objectContaining({ cache: 'no-store' }),
    )
    const s = useSprintWorkspaceStore.getState()
    expect(s.relations.sprintIdsByProduct['prod-1']).toEqual(['sp-new', 'sp-old'])
    expect(s.loading.loadedProductSprintsIds['prod-1']).toBe(true)
  })
})

describe('ensureSprintLoaded', () => {
  it('fetcht sprint-snapshot en hydreert', async () => {
    const sprint = makeSprint({ id: 'sp-1', product_id: 'prod-1' })
    const snapshot: SprintWorkspaceSnapshot = {
      product: { id: 'prod-1', name: 'P1' },
      sprint,
      stories: [makeStory({ id: 's-1', pbi_id: 'pbi-1', sprint_id: 'sp-1' })],
      tasksByStory: {
        's-1': [makeTask({ id: 't-1', story_id: 's-1' })],
      },
    }
    const fetchSpy = mockFetchSequence([snapshot])

    await useSprintWorkspaceStore.getState().ensureSprintLoaded('sp-1')

    expect(fetchSpy).toHaveBeenCalledWith(
      '/api/sprints/sp-1/workspace',
      expect.objectContaining({ cache: 'no-store' }),
    )
    const s = useSprintWorkspaceStore.getState()
    expect(s.entities.sprintsById['sp-1']).toBeDefined()
    expect(s.relations.storyIdsBySprint['sp-1']).toEqual(['s-1'])
    expect(s.relations.taskIdsByStory['s-1']).toEqual(['t-1'])
    expect(s.loading.loadedSprintIds['sp-1']).toBe(true)
  })
})

describe('race-safe ensure*Loaded — activeRequestId guard', () => {
  it('oudere in-flight ensureSprintLoaded mag nieuwere selectie niet overschrijven', async () => {
    let resolveOld: ((snap: SprintWorkspaceSnapshot) => void) | null = null

    vi.spyOn(globalThis, 'fetch').mockImplementation((async (url: string) => {
      if (url === '/api/sprints/sp-old/workspace') {
        const snap = await new Promise<SprintWorkspaceSnapshot>((resolve) => {
          resolveOld = resolve
        })
        return new Response(JSON.stringify(snap), { status: 200 })
      }
      if (url === '/api/sprints/sp-new/workspace') {
        return new Response(
          JSON.stringify({
            sprint: makeSprint({ id: 'sp-new', product_id: 'prod-1' }),
            stories: [makeStory({ id: 'new-st', pbi_id: 'p', sprint_id: 'sp-new' })],
            tasksByStory: {},
          }),
          { status: 200 },
        )
      }
      return new Response('null', { status: 200 })
    }) as unknown as typeof fetch)

    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
      s.context.activeSprintId = 'sp-old'
      s.loading.activeRequestId = 'req-old'
    })
    const oldPromise = useSprintWorkspaceStore
      .getState()
      .ensureSprintLoaded('sp-old', 'req-old')

    useSprintWorkspaceStore.setState((s) => {
      s.context.activeSprintId = 'sp-new'
      s.loading.activeRequestId = 'req-new'
    })
    await useSprintWorkspaceStore
      .getState()
      .ensureSprintLoaded('sp-new', 'req-new')

    expect(useSprintWorkspaceStore.getState().entities.storiesById['new-st']).toBeDefined()

    resolveOld!({
      sprint: makeSprint({ id: 'sp-old', product_id: 'prod-1' }),
      stories: [makeStory({ id: 'old-st', pbi_id: 'p', sprint_id: 'sp-old' })],
      tasksByStory: {},
    })
    await oldPromise

    const s = useSprintWorkspaceStore.getState()
    expect(s.context.activeSprintId).toBe('sp-new')
    expect(s.entities.storiesById['old-st']).toBeUndefined()
    expect(s.entities.storiesById['new-st']).toBeDefined()
  })
})

describe('ensureTaskLoaded — zet detail-flag', () => {
  it('verrijkt task naar TaskDetail met _detail: true', async () => {
    mockFetchSequence([
      {
        id: 't-1',
        code: 'C1',
        title: 'Task 1',
        description: 'desc',
        priority: 1,
        sort_order: 1,
        status: 'todo',
        story_id: 's-1',
        sprint_id: 'sp-1',
        created_at: new Date('2026-02-01').toISOString(),
        implementation_plan: 'detailed plan here',
      },
    ])

    await useSprintWorkspaceStore.getState().ensureTaskLoaded('t-1')
    const task = useSprintWorkspaceStore.getState().entities.tasksById[
      't-1'
    ] as SprintWorkspaceTaskDetail
    expect(task._detail).toBe(true)
    expect(task.status).toBe('TO_DO')
    expect(task.implementation_plan).toBe('detailed plan here')
    expect(useSprintWorkspaceStore.getState().loading.loadedTaskIds['t-1']).toBe(true)
  })
})

// ─────────────────────────────────────────────────────────────────────────
// resyncActiveScopes
// ─────────────────────────────────────────────────────────────────────────

describe('resyncActiveScopes', () => {
  it('triggert ensure-keten voor alle actieve scopes en zet sync velden', async () => {
    const fetchSpy = mockFetchSequence([
      // ensureProductSprintsLoaded
      [],
      // ensureSprintLoaded
      {
        sprint: makeSprint({ id: 'sp-1', product_id: 'prod-1' }),
        stories: [],
        tasksByStory: {},
      },
      // ensureStoryLoaded
      [],
      // ensureTaskLoaded
      {
        id: 't-1',
        title: 'T',
        description: null,
        priority: 1,
        sort_order: 1,
        status: 'todo',
        story_id: 's-1',
        sprint_id: 'sp-1',
        created_at: '2026-02-01',
      },
    ])

    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'P' }
      s.context.activeSprintId = 'sp-1'
      s.context.activeStoryId = 's-1'
      s.context.activeTaskId = 't-1'
    })

    await useSprintWorkspaceStore.getState().resyncActiveScopes('manual')

    const calls = fetchSpy.mock.calls.map(([url]) => url)
    expect(calls).toContain('/api/products/prod-1/sprints')
    expect(calls).toContain('/api/sprints/sp-1/workspace')
    expect(calls).toContain('/api/stories/s-1/tasks')
    expect(calls).toContain('/api/tasks/t-1')

    const s = useSprintWorkspaceStore.getState()
    expect(s.sync.lastResyncAt).toBeTypeOf('number')
    expect(s.sync.resyncReason).toBe('manual')
  })
})

// ─────────────────────────────────────────────────────────────────────────
// Restore-hint flow
// ─────────────────────────────────────────────────────────────────────────

describe('restore-hint flow — setters persisteren hints', () => {
  it('setActiveProduct schrijft lastActiveProductId', () => {
    useSprintWorkspaceStore.getState().setActiveProduct({ id: 'prod-1', name: 'P1' })
    const raw = localStorage.getItem('sprint-workspace-hints')
    expect(raw).not.toBeNull()
    const hints = JSON.parse(raw!)
    expect(hints.lastActiveProductId).toBe('prod-1')
  })

  it('setActiveSprint schrijft lastActiveSprintId per product', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'P1' }
    })
    useSprintWorkspaceStore.getState().setActiveSprint('sp-a')
    const hints = JSON.parse(localStorage.getItem('sprint-workspace-hints')!)
    expect(hints.perProduct['prod-1'].lastActiveSprintId).toBe('sp-a')
  })

  it('setActiveStory schrijft lastActiveStoryId per sprint', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeSprintId = 'sp-1'
    })
    useSprintWorkspaceStore.getState().setActiveStory('s-a')
    const hints = JSON.parse(localStorage.getItem('sprint-workspace-hints')!)
    expect(hints.perSprint['sp-1'].lastActiveStoryId).toBe('s-a')
  })

  it('setActiveTask schrijft lastActiveTaskId per sprint', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeSprintId = 'sp-1'
    })
    useSprintWorkspaceStore.getState().setActiveTask('t-a')
    const hints = JSON.parse(localStorage.getItem('sprint-workspace-hints')!)
    expect(hints.perSprint['sp-1'].lastActiveTaskId).toBe('t-a')
  })
})

describe('restore-hint flow — chain triggert na ensure*Loaded', () => {
  it('hint die NIET in entities zit wordt genegeerd', async () => {
    localStorage.setItem(
      'sprint-workspace-hints',
      JSON.stringify({
        lastActiveProductId: 'prod-1',
        perProduct: { 'prod-1': { lastActiveSprintId: 'ghost-sprint' } },
        perSprint: {},
      }),
    )
    mockFetchSequence([[]])

    useSprintWorkspaceStore.getState().setActiveProduct({ id: 'prod-1', name: 'P1' })
    await new Promise((r) => setTimeout(r, 20))

    expect(useSprintWorkspaceStore.getState().context.activeSprintId).toBeNull()
  })

  it('hint die wel in entities zit wordt toegepast', async () => {
    const validSprint = makeSprint({ id: 'sp-known', product_id: 'prod-1' })
    localStorage.setItem(
      'sprint-workspace-hints',
      JSON.stringify({
        lastActiveProductId: 'prod-1',
        perProduct: { 'prod-1': { lastActiveSprintId: 'sp-known' } },
        perSprint: {},
      }),
    )
    mockFetchSequence([
      // ensureProductSprintsLoaded — levert sp-known
      [validSprint],
      // ensureSprintLoaded triggered door setActiveSprint(hint)
      { sprint: validSprint, stories: [], tasksByStory: {} },
    ])

    useSprintWorkspaceStore.getState().setActiveProduct({ id: 'prod-1', name: 'P1' })
    await new Promise((r) => setTimeout(r, 30))

    expect(useSprintWorkspaceStore.getState().context.activeSprintId).toBe('sp-known')
  })
})

// ─────────────────────────────────────────────────────────────────────────
// Optimistic mutations
// ─────────────────────────────────────────────────────────────────────────

describe('optimistic mutations', () => {
  it('SSE-echo van een al-bestaande sprint is idempotent', () => {
    useSprintWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'P' }
    })
    useSprintWorkspaceStore
      .getState()
      .hydrateProductSprints('prod-1', [
        makeSprint({ id: 'sp-1', product_id: 'prod-1', sprint_goal: 'Origineel' }),
      ])
    useSprintWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'sprint',
      op: 'I',
      id: 'sp-1',
      product_id: 'prod-1',
      sprint_goal: 'echo',
    })
    expect(
      useSprintWorkspaceStore.getState().entities.sprintsById['sp-1'].sprint_goal,
    ).toBe('Origineel')
  })
})
