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

import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
import type {
  BacklogPbi,
  BacklogStory,
  BacklogTask,
  ProductBacklogSnapshot,
  TaskDetail,
} from '@/stores/product-workspace/types'

// G5: snapshot original actions on module-load; restore in beforeEach.
// vi.fn-spies on actions could leak across tests otherwise.
const originalActions = (() => {
  const s = useProductWorkspaceStore.getState()
  return {
    hydrateSnapshot: s.hydrateSnapshot,
    setActiveProduct: s.setActiveProduct,
    setActivePbi: s.setActivePbi,
    setActiveStory: s.setActiveStory,
    setActiveTask: s.setActiveTask,
    ensureProductLoaded: s.ensureProductLoaded,
    ensurePbiLoaded: s.ensurePbiLoaded,
    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() {
  useProductWorkspaceStore.setState((s) => {
    s.context.activeProduct = null
    s.context.activePbiId = null
    s.context.activeStoryId = null
    s.context.activeTaskId = null
    s.entities.pbisById = {}
    s.entities.storiesById = {}
    s.entities.tasksById = {}
    s.relations.pbiIds = []
    s.relations.storyIdsByPbi = {}
    s.relations.taskIdsByStory = {}
    s.loading.loadedProductId = null
    s.loading.loadingProductId = null
    s.loading.loadedPbiIds = {}
    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 = {}
    s.sprintMembership = {
      pbiSummary: {},
      crossSprintBlocks: {},
      pending: { adds: [], removes: [] },
      loadedSummaryForSprintId: null,
    }
    Object.assign(s, originalActions)
  })
}

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

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

function makePbi(overrides: Partial<BacklogPbi> & { id: string }): BacklogPbi {
  return {
    id: overrides.id,
    code: overrides.code ?? overrides.id,
    title: overrides.title ?? `PBI ${overrides.id}`,
    priority: overrides.priority ?? 2,
    sort_order: overrides.sort_order ?? 1,
    description: overrides.description ?? null,
    created_at: overrides.created_at ?? new Date('2026-01-01'),
    status: overrides.status ?? 'ready',
  }
}

function makeStory(overrides: Partial<BacklogStory> & { id: string; pbi_id: string }): BacklogStory {
  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<BacklogTask> & { id: string; story_id: string }): BacklogTask {
  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,
    created_at: overrides.created_at ?? new Date('2026-01-01'),
  }
}

function snapshotWith(
  pbis: BacklogPbi[],
  storiesByPbi: Record<string, BacklogStory[]> = {},
  tasksByStory: Record<string, BacklogTask[]> = {},
  product?: { id: string; name: string },
): ProductBacklogSnapshot {
  return { product, pbis, storiesByPbi, tasksByStory }
}

// G7: mock fetch — never let it fall through to real network
// G8: mockImplementation per call so each fetch gets a fresh Response
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 en relations met gesorteerde id-lijsten', () => {
    const pbiA = makePbi({ id: 'pbi-a', priority: 2, sort_order: 2 })
    const pbiB = makePbi({ id: 'pbi-b', priority: 1, sort_order: 5 })
    const pbiC = makePbi({ id: 'pbi-c', priority: 2, sort_order: 1 })
    const storyB1 = makeStory({ id: 'st-1', pbi_id: 'pbi-b', sort_order: 2 })
    const storyB2 = makeStory({ id: 'st-2', pbi_id: 'pbi-b', sort_order: 1 })
    const taskA = makeTask({ id: 'tk-2', story_id: 'st-1', sort_order: 2 })
    const taskB = makeTask({ id: 'tk-1', story_id: 'st-1', sort_order: 1 })

    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [pbiA, pbiB, pbiC],
        { 'pbi-b': [storyB1, storyB2] },
        { 'st-1': [taskA, taskB] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )

    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['pbi-a']).toBe(pbiA)
    expect(s.entities.pbisById['pbi-b']).toBe(pbiB)
    expect(s.entities.pbisById['pbi-c']).toBe(pbiC)
    // pbi-b heeft priority 1 (komt eerst), dan pbi-c (sort_order 1) en pbi-a (sort_order 2)
    expect(s.relations.pbiIds).toEqual(['pbi-b', 'pbi-c', 'pbi-a'])
    expect(s.relations.storyIdsByPbi['pbi-b']).toEqual(['st-2', 'st-1'])
    expect(s.relations.taskIdsByStory['st-1']).toEqual(['tk-1', 'tk-2'])
    expect(s.context.activeProduct).toEqual({ id: 'prod-1', name: 'Product 1' })
    expect(s.loading.loadedProductId).toBe('prod-1')
  })

  it('normaliseert API-statussen naar het interne store-contract', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'pbi-1', status: 'READY' as BacklogPbi['status'] })],
        {
          'pbi-1': [
            makeStory({ id: 'st-1', pbi_id: 'pbi-1', status: 'in_sprint' }),
          ],
        },
        {
          'st-1': [makeTask({ id: 'tk-1', story_id: 'st-1', status: 'todo' })],
        },
      ),
    )

    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['pbi-1'].status).toBe('ready')
    expect(s.entities.storiesById['st-1'].status).toBe('IN_SPRINT')
    expect(s.entities.tasksById['tk-1'].status).toBe('TO_DO')
  })

  it('reset bestaande entities en relations bij her-hydratie', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'old-pbi' })]),
    )
    expect(useProductWorkspaceStore.getState().relations.pbiIds).toEqual(['old-pbi'])

    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'new-pbi' })]),
    )
    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['old-pbi']).toBeUndefined()
    expect(s.entities.pbisById['new-pbi']).toBeDefined()
    expect(s.relations.pbiIds).toEqual(['new-pbi'])
  })
})

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

describe('selection cascade', () => {
  it('setActivePbi reset story+task; setActiveStory reset task', () => {
    useProductWorkspaceStore.setState((s) => {
      s.context.activePbiId = 'pbi-old'
      s.context.activeStoryId = 'st-old'
      s.context.activeTaskId = 'tk-old'
    })

    useProductWorkspaceStore.getState().setActivePbi('pbi-new')
    let s = useProductWorkspaceStore.getState()
    expect(s.context.activePbiId).toBe('pbi-new')
    expect(s.context.activeStoryId).toBeNull()
    expect(s.context.activeTaskId).toBeNull()

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

  it('setActiveProduct(null) ruimt entities en relations op', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'p-1' })],
        { 'p-1': [makeStory({ id: 's-1', pbi_id: 'p-1' })] },
        { 's-1': [makeTask({ id: 't-1', story_id: 's-1' })] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )

    useProductWorkspaceStore.getState().setActiveProduct(null)
    const s = useProductWorkspaceStore.getState()
    expect(s.context.activeProduct).toBeNull()
    expect(s.context.activePbiId).toBeNull()
    expect(s.context.activeStoryId).toBeNull()
    expect(s.context.activeTaskId).toBeNull()
    expect(s.entities.pbisById).toEqual({})
    expect(s.entities.storiesById).toEqual({})
    expect(s.entities.tasksById).toEqual({})
    expect(s.relations.pbiIds).toEqual([])
    expect(s.relations.storyIdsByPbi).toEqual({})
    expect(s.relations.taskIdsByStory).toEqual({})
    expect(s.loading.loadedProductId).toBeNull()
  })

  it('setActiveProduct kan alleen context zetten zonder full backlog load', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'p-1' })],
        { 'p-1': [makeStory({ id: 's-1', pbi_id: 'p-1' })] },
        { 's-1': [makeTask({ id: 't-1', story_id: 's-1' })] },
        { id: 'prod-1', name: 'Product 1' },
      ),
    )
    useProductWorkspaceStore.setState((s) => {
      s.context.activePbiId = 'p-1'
      s.context.activeStoryId = 's-1'
    })
    const fetchSpy = vi.spyOn(globalThis, 'fetch')

    useProductWorkspaceStore
      .getState()
      .setActiveProduct(
        { id: 'prod-1', name: 'Product 1' },
        { load: false, preserveSelection: true },
      )

    const s = useProductWorkspaceStore.getState()
    expect(fetchSpy).not.toHaveBeenCalled()
    expect(s.context.activePbiId).toBe('p-1')
    expect(s.context.activeStoryId).toBe('s-1')
    expect(s.entities.pbisById['p-1']).toBeDefined()
  })
})

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

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

  it('I — voegt PBI toe en sorteert', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'a', priority: 2, sort_order: 5 })]),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'I',
      id: 'b',
      product_id: 'prod-1',
      code: 'B',
      title: 'New PBI',
      priority: 1,
      sort_order: 1,
      created_at: new Date('2026-02-01').toISOString(),
      status: 'ready',
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['b']).toBeDefined()
    expect(s.relations.pbiIds).toEqual(['b', 'a'])
  })

  it('I — idempotent voor bestaande id', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'a' })]),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'I',
      id: 'a',
      product_id: 'prod-1',
      title: 'mutated',
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['a'].title).toBe('PBI a') // niet overschreven
    expect(s.relations.pbiIds).toEqual(['a'])
  })

  it('U — patch + her-sorteert', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([
        makePbi({ id: 'a', priority: 2, sort_order: 1 }),
        makePbi({ id: 'b', priority: 2, sort_order: 2 }),
      ]),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'U',
      id: 'b',
      product_id: 'prod-1',
      priority: 1,
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.relations.pbiIds).toEqual(['b', 'a'])
  })

  it('D — verwijdert PBI inclusief child stories en tasks', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'p1' })],
        { p1: [makeStory({ id: 's1', pbi_id: 'p1' })] },
        { s1: [makeTask({ id: 't1', story_id: 's1' })] },
      ),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'D',
      id: 'p1',
      product_id: 'prod-1',
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['p1']).toBeUndefined()
    expect(s.entities.storiesById['s1']).toBeUndefined()
    expect(s.entities.tasksById['t1']).toBeUndefined()
    expect(s.relations.pbiIds).toEqual([])
    expect(s.relations.storyIdsByPbi['p1']).toBeUndefined()
    expect(s.relations.taskIdsByStory['s1']).toBeUndefined()
  })

  it('D — clear actieve PBI selectie als die onder de verwijderde PBI viel', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'p1' })]),
    )
    useProductWorkspaceStore.setState((s) => {
      s.context.activePbiId = 'p1'
      s.context.activeStoryId = 's-x'
      s.context.activeTaskId = 't-x'
    })
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'D',
      id: 'p1',
      product_id: 'prod-1',
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.context.activePbiId).toBeNull()
    expect(s.context.activeStoryId).toBeNull()
    expect(s.context.activeTaskId).toBeNull()
  })
})

describe('applyRealtimeEvent — story parent-move', () => {
  beforeEach(() => {
    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'p1' }), makePbi({ id: 'p2' })],
        {
          p1: [makeStory({ id: 's1', pbi_id: 'p1' })],
          p2: [],
        },
      ),
    )
  })

  it('U met andere pbi_id verplaatst story naar nieuwe parent-lijst', () => {
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'story',
      op: 'U',
      id: 's1',
      product_id: 'prod-1',
      pbi_id: 'p2',
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.relations.storyIdsByPbi['p1']).toEqual([])
    expect(s.relations.storyIdsByPbi['p2']).toEqual(['s1'])
    expect(s.entities.storiesById['s1'].pbi_id).toBe('p2')
  })
})

describe('applyRealtimeEvent — task parent-move', () => {
  beforeEach(() => {
    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith(
        [makePbi({ id: 'p1' })],
        { p1: [makeStory({ id: 's1', pbi_id: 'p1' }), makeStory({ id: 's2', pbi_id: 'p1' })] },
        {
          s1: [makeTask({ id: 't1', story_id: 's1' })],
          s2: [],
        },
      ),
    )
  })

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

describe('applyRealtimeEvent — andere product genegeerd', () => {
  it('event met ander product_id raakt de store niet', () => {
    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
    })
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'a' })]),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'I',
      id: 'b',
      product_id: 'prod-2',
      title: 'Other product',
      priority: 1,
      sort_order: 1,
    })
    const s = useProductWorkspaceStore.getState()
    expect(s.entities.pbisById['b']).toBeUndefined()
    expect(s.relations.pbiIds).toEqual(['a'])
  })
})

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

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

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

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

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

  it('claude_job_enqueued (type-veld) triggert geen resync', () => {
    const spy = withSpy()
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      type: 'claude_job_enqueued',
      job_id: 'job-2',
      product_id: 'prod-1',
      kind: 'PER_TASK',
    } as unknown as Record<string, unknown>)
    expect(spy).not.toHaveBeenCalled()
  })

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

  it('question-event met entity-veld maar zonder pbi/story/task triggert resync', () => {
    // question is geen pbi/story/task entity dus telt als unknown wanneer
    // hij geen 'type' draagt — dat zou een nieuwe entiteit kunnen zijn die
    // we nog niet kennen.
    const spy = withSpy()
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'question',
      op: 'I',
      id: 'q-1',
      product_id: 'prod-1',
    } as unknown as Record<string, unknown>)
    expect(spy).toHaveBeenCalledWith('unknown-event')
  })
})

// ─────────────────────────────────────────────────────────────────────────
// ensure*Loaded fetches + race-safe guard + sortering
// ─────────────────────────────────────────────────────────────────────────

describe('ensureProductLoaded', () => {
  it('fetcht backlog snapshot en hydreert met sortering', async () => {
    const snapshot: ProductBacklogSnapshot = {
      product: { id: 'prod-1', name: 'Product 1' },
      pbis: [
        makePbi({ id: 'a', priority: 2, sort_order: 5 }),
        makePbi({ id: 'b', priority: 1, sort_order: 9 }),
      ],
      storiesByPbi: {},
      tasksByStory: {},
    }
    const fetchSpy = mockFetchSequence([snapshot])

    await useProductWorkspaceStore.getState().ensureProductLoaded('prod-1')

    expect(fetchSpy).toHaveBeenCalledWith(
      '/api/products/prod-1/backlog',
      expect.objectContaining({ cache: 'no-store' }),
    )
    const s = useProductWorkspaceStore.getState()
    expect(s.relations.pbiIds).toEqual(['b', 'a'])
    expect(s.loading.loadedProductId).toBe('prod-1')
    expect(s.loading.loadedPbiIds['a']).toBe(true)
    expect(s.loading.loadedPbiIds['b']).toBe(true)
  })
})

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

    vi.spyOn(globalThis, 'fetch').mockImplementation((async (url: string) => {
      if (url === '/api/pbis/pbi-old/stories') {
        const stories = await new Promise<BacklogStory[]>((resolve) => {
          resolveOld = resolve
        })
        return new Response(JSON.stringify(stories), { status: 200 })
      }
      if (url === '/api/pbis/pbi-new/stories') {
        return new Response(
          JSON.stringify([makeStory({ id: 'new-st', pbi_id: 'pbi-new' })]),
          { status: 200 },
        )
      }
      return new Response('null', { status: 200 })
    }) as unknown as typeof fetch)

    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'Product 1' }
      s.context.activePbiId = 'pbi-old'
      s.loading.activeRequestId = 'req-old'
    })
    const oldPromise = useProductWorkspaceStore
      .getState()
      .ensurePbiLoaded('pbi-old', 'req-old')

    // gebruiker selecteert ondertussen pbi-new
    useProductWorkspaceStore.setState((s) => {
      s.context.activePbiId = 'pbi-new'
      s.loading.activeRequestId = 'req-new'
    })
    await useProductWorkspaceStore.getState().ensurePbiLoaded('pbi-new', 'req-new')

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

    // resolve de oude fetch — guard moet de stale data weigeren
    resolveOld!([makeStory({ id: 'old-st', pbi_id: 'pbi-old' })])
    await oldPromise

    const s = useProductWorkspaceStore.getState()
    expect(s.context.activePbiId).toBe('pbi-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',
        title: 'Task 1',
        description: 'desc',
        priority: 1,
        sort_order: 1,
        status: 'todo',
        story_id: 's-1',
        created_at: new Date('2026-02-01').toISOString(),
        implementation_plan: 'detailed plan here',
      },
    ])

    await useProductWorkspaceStore.getState().ensureTaskLoaded('t-1')
    const task = useProductWorkspaceStore.getState().entities.tasksById['t-1'] as TaskDetail
    expect(task._detail).toBe(true)
    expect(task.status).toBe('TO_DO')
    expect(task.implementation_plan).toBe('detailed plan here')
    expect(useProductWorkspaceStore.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([
      // ensureProductLoaded
      { product: { id: 'prod-1', name: 'P' }, pbis: [], storiesByPbi: {}, tasksByStory: {} },
      // ensurePbiLoaded
      [],
      // ensureStoryLoaded
      [],
      // ensureTaskLoaded
      {
        id: 't-1',
        title: 'T',
        description: null,
        priority: 1,
        sort_order: 1,
        status: 'todo',
        story_id: 's-1',
        created_at: '2026-02-01',
      },
    ])

    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'P' }
      s.context.activePbiId = 'pbi-1'
      s.context.activeStoryId = 's-1'
      s.context.activeTaskId = 't-1'
    })

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

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

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

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

// ─────────────────────────────────────────────────────────────────────────
// Restore-hint integratie (Story 4)
// ─────────────────────────────────────────────────────────────────────────

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

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

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

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

describe('restore-hint flow — chain triggert na ensure*Loaded', () => {
  it('hint die NIET in entities zit wordt genegeerd', async () => {
    // Schrijf een hint voor een PBI die niet bestaat
    localStorage.setItem(
      'product-workspace-hints',
      JSON.stringify({
        lastActiveProductId: 'prod-1',
        perProduct: { 'prod-1': { lastActivePbiId: 'ghost-pbi' } },
      }),
    )
    // Mock ensureProductLoaded zodat hij een lege snapshot terugstuurt — geen
    // ghost-pbi in entities.
    mockFetchSequence([
      { product: { id: 'prod-1', name: 'P1' }, pbis: [], storiesByPbi: {}, tasksByStory: {} },
    ])

    useProductWorkspaceStore.getState().setActiveProduct({ id: 'prod-1', name: 'P1' })
    // Wacht tot async restore-flow afgewikkeld is.
    await new Promise((r) => setTimeout(r, 20))

    expect(useProductWorkspaceStore.getState().context.activePbiId).toBeNull()
  })

  it('hint die wel in entities zit wordt toegepast', async () => {
    const validPbi = makePbi({ id: 'pbi-known' })
    localStorage.setItem(
      'product-workspace-hints',
      JSON.stringify({
        lastActiveProductId: 'prod-1',
        perProduct: { 'prod-1': { lastActivePbiId: 'pbi-known' } },
      }),
    )
    mockFetchSequence([
      // ensureProductLoaded levert pbi-known
      {
        product: { id: 'prod-1', name: 'P1' },
        pbis: [validPbi],
        storiesByPbi: {},
        tasksByStory: {},
      },
      // ensurePbiLoaded triggered door setActivePbi(hint) — geen stories
      [],
    ])

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

    expect(useProductWorkspaceStore.getState().context.activePbiId).toBe('pbi-known')
  })
})

describe('optimistic mutations', () => {
  it('rollback herstelt vorige pbi-order', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([
        makePbi({ id: 'a', priority: 2, sort_order: 1 }),
        makePbi({ id: 'b', priority: 2, sort_order: 2 }),
      ]),
    )
    const prevOrder = [...useProductWorkspaceStore.getState().relations.pbiIds]

    const id = useProductWorkspaceStore.getState().applyOptimisticMutation({
      kind: 'pbi-order',
      prevPbiIds: prevOrder,
    })
    // simuleer de optimistic order-wijziging buiten de mutation
    useProductWorkspaceStore.setState((s) => {
      s.relations.pbiIds = ['b', 'a']
    })
    expect(useProductWorkspaceStore.getState().relations.pbiIds).toEqual(['b', 'a'])

    useProductWorkspaceStore.getState().rollbackMutation(id)
    expect(useProductWorkspaceStore.getState().relations.pbiIds).toEqual(prevOrder)
    expect(useProductWorkspaceStore.getState().pendingMutations[id]).toBeUndefined()
  })

  it('settle ruimt pending op zonder state te wijzigen', () => {
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'a' })]),
    )
    const id = useProductWorkspaceStore.getState().applyOptimisticMutation({
      kind: 'pbi-order',
      prevPbiIds: ['a'],
    })
    expect(useProductWorkspaceStore.getState().pendingMutations[id]).toBeDefined()

    useProductWorkspaceStore.getState().settleMutation(id)
    expect(useProductWorkspaceStore.getState().pendingMutations[id]).toBeUndefined()
    expect(useProductWorkspaceStore.getState().relations.pbiIds).toEqual(['a'])
  })

  it('SSE-echo van een al-bestaande PBI is idempotent', () => {
    useProductWorkspaceStore.setState((s) => {
      s.context.activeProduct = { id: 'prod-1', name: 'P' }
    })
    useProductWorkspaceStore.getState().hydrateSnapshot(
      snapshotWith([makePbi({ id: 'a', title: 'Origineel' })]),
    )
    useProductWorkspaceStore.getState().applyRealtimeEvent({
      entity: 'pbi',
      op: 'I',
      id: 'a',
      product_id: 'prod-1',
      title: 'echo',
    })
    expect(useProductWorkspaceStore.getState().entities.pbisById['a'].title).toBe('Origineel')
    expect(useProductWorkspaceStore.getState().relations.pbiIds).toEqual(['a'])
  })
})
