// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
import {
  selectIsDirty,
  selectPbiTriState,
  selectPendingCount,
  selectStoryEffectiveInSprint,
  selectStoryIsBlocked,
} from '@/stores/product-workspace/selectors'
import type { BacklogStory } from '@/stores/product-workspace/types'

function resetMembership() {
  useProductWorkspaceStore.setState((s) => {
    s.entities.storiesById = {}
    s.relations.storyIdsByPbi = {}
    s.sprintMembership = {
      pbiSummary: {},
      crossSprintBlocks: {},
      pending: { adds: [], removes: [] },
      loadedSummaryForSprintId: null,
    }
  })
}

function seedStory(id: string, pbiId: string, sprintId: string | null): BacklogStory {
  return {
    id,
    code: id,
    title: id,
    description: null,
    acceptance_criteria: null,
    priority: 2,
    sort_order: 1,
    status: sprintId ? 'IN_SPRINT' : 'OPEN',
    pbi_id: pbiId,
    sprint_id: sprintId,
    created_at: new Date('2026-01-01'),
  }
}

beforeEach(() => {
  resetMembership()
})

describe('toggleStorySprintMembership', () => {
  it('adds storyId to pending.adds when currently not in sprint', () => {
    useProductWorkspaceStore.getState().toggleStorySprintMembership('s1', false)
    const pending = useProductWorkspaceStore.getState().sprintMembership.pending
    expect(pending.adds).toEqual(['s1'])
    expect(pending.removes).toEqual([])
  })

  it('adds storyId to pending.removes when currently in sprint', () => {
    useProductWorkspaceStore.getState().toggleStorySprintMembership('s1', true)
    const pending = useProductWorkspaceStore.getState().sprintMembership.pending
    expect(pending.removes).toEqual(['s1'])
    expect(pending.adds).toEqual([])
  })

  it('cancels out: toggle add → toggle remove same story (in-sprint) clears pending', () => {
    const store = useProductWorkspaceStore.getState()
    store.toggleStorySprintMembership('s1', false) // adds
    // Story now appears to be "in sprint" via pending; calling with true should cancel
    store.toggleStorySprintMembership('s1', false) // second click with same baseline
    const pending = useProductWorkspaceStore.getState().sprintMembership.pending
    expect(pending.adds).toEqual([])
    expect(pending.removes).toEqual([])
  })

  it('removes from pending.removes when toggled back', () => {
    const store = useProductWorkspaceStore.getState()
    store.toggleStorySprintMembership('s1', true)
    store.toggleStorySprintMembership('s1', true)
    const pending = useProductWorkspaceStore.getState().sprintMembership.pending
    expect(pending.removes).toEqual([])
    expect(pending.adds).toEqual([])
  })

  it('resetSprintMembershipPending empties both arrays', () => {
    const store = useProductWorkspaceStore.getState()
    store.toggleStorySprintMembership('s1', false)
    store.toggleStorySprintMembership('s2', true)
    store.resetSprintMembershipPending()
    const pending = useProductWorkspaceStore.getState().sprintMembership.pending
    expect(pending.adds).toEqual([])
    expect(pending.removes).toEqual([])
  })
})

describe('selectPbiTriState', () => {
  function seedSummary(pbiId: string, total: number, inSprint: number) {
    useProductWorkspaceStore.setState((s) => {
      s.sprintMembership.pbiSummary[pbiId] = {
        totalStoryCount: total,
        inActiveSprintStoryCount: inSprint,
      }
    })
  }

  it('returns empty for PBI without summary', () => {
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('empty')
  })

  it('returns empty when totalStoryCount == 0', () => {
    seedSummary('pbi-1', 0, 0)
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('empty')
  })

  it('returns full when all stories in sprint (no pending)', () => {
    seedSummary('pbi-1', 3, 3)
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('full')
  })

  it('returns partial when some stories in sprint', () => {
    seedSummary('pbi-1', 3, 2)
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('partial')
  })

  it('returns empty when inSprint == 0', () => {
    seedSummary('pbi-1', 3, 0)
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('empty')
  })

  it('applies pending adds when stories are loaded for the PBI', () => {
    seedSummary('pbi-1', 3, 1)
    useProductWorkspaceStore.setState((s) => {
      s.relations.storyIdsByPbi['pbi-1'] = ['s1', 's2', 's3']
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', 'sprint-1')
      s.entities.storiesById['s2'] = seedStory('s2', 'pbi-1', null)
      s.entities.storiesById['s3'] = seedStory('s3', 'pbi-1', null)
      s.sprintMembership.pending.adds = ['s2', 's3']
    })
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('full')
  })

  it('applies pending removes when stories are loaded for the PBI', () => {
    seedSummary('pbi-1', 3, 3)
    useProductWorkspaceStore.setState((s) => {
      s.relations.storyIdsByPbi['pbi-1'] = ['s1', 's2', 's3']
      s.sprintMembership.pending.removes = ['s2']
    })
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('partial')
  })

  it('ignores pending entries for stories of other PBIs', () => {
    seedSummary('pbi-1', 3, 3)
    useProductWorkspaceStore.setState((s) => {
      s.relations.storyIdsByPbi['pbi-1'] = ['s1', 's2', 's3']
      s.sprintMembership.pending.removes = ['s99'] // not in pbi-1
    })
    expect(
      selectPbiTriState(useProductWorkspaceStore.getState(), 'pbi-1'),
    ).toBe('full')
  })
})

describe('selectStoryEffectiveInSprint', () => {
  it('returns true when story.sprint_id matches activeSprintId and no pending', () => {
    useProductWorkspaceStore.setState((s) => {
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', 'sprint-A')
    })
    expect(
      selectStoryEffectiveInSprint(
        useProductWorkspaceStore.getState(),
        's1',
        'sprint-A',
      ),
    ).toBe(true)
  })

  it('returns false when story.sprint_id is null', () => {
    useProductWorkspaceStore.setState((s) => {
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', null)
    })
    expect(
      selectStoryEffectiveInSprint(
        useProductWorkspaceStore.getState(),
        's1',
        'sprint-A',
      ),
    ).toBe(false)
  })

  it('returns true when story in pending.adds even if DB says no', () => {
    useProductWorkspaceStore.setState((s) => {
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', null)
      s.sprintMembership.pending.adds = ['s1']
    })
    expect(
      selectStoryEffectiveInSprint(
        useProductWorkspaceStore.getState(),
        's1',
        'sprint-A',
      ),
    ).toBe(true)
  })

  it('returns false when story in pending.removes even if DB says yes', () => {
    useProductWorkspaceStore.setState((s) => {
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', 'sprint-A')
      s.sprintMembership.pending.removes = ['s1']
    })
    expect(
      selectStoryEffectiveInSprint(
        useProductWorkspaceStore.getState(),
        's1',
        'sprint-A',
      ),
    ).toBe(false)
  })

  it('returns false when activeSprintId is null', () => {
    useProductWorkspaceStore.setState((s) => {
      s.entities.storiesById['s1'] = seedStory('s1', 'pbi-1', 'sprint-A')
    })
    expect(
      selectStoryEffectiveInSprint(
        useProductWorkspaceStore.getState(),
        's1',
        null,
      ),
    ).toBe(false)
  })
})

describe('selectStoryIsBlocked', () => {
  it('returns null when no block', () => {
    expect(
      selectStoryIsBlocked(useProductWorkspaceStore.getState(), 's1'),
    ).toBeNull()
  })

  it('returns block info when story is in another sprint', () => {
    useProductWorkspaceStore.setState((s) => {
      s.sprintMembership.crossSprintBlocks['s1'] = {
        sprintId: 'sprint-x',
        sprintName: 'SP-X',
      }
    })
    expect(
      selectStoryIsBlocked(useProductWorkspaceStore.getState(), 's1'),
    ).toEqual({ sprintId: 'sprint-x', sprintName: 'SP-X' })
  })
})

describe('selectIsDirty + selectPendingCount', () => {
  it('clean by default', () => {
    expect(selectIsDirty(useProductWorkspaceStore.getState())).toBe(false)
    expect(selectPendingCount(useProductWorkspaceStore.getState())).toBe(0)
  })

  it('counts adds + removes', () => {
    useProductWorkspaceStore.setState((s) => {
      s.sprintMembership.pending = {
        adds: ['a1', 'a2'],
        removes: ['r1'],
      }
    })
    expect(selectIsDirty(useProductWorkspaceStore.getState())).toBe(true)
    expect(selectPendingCount(useProductWorkspaceStore.getState())).toBe(3)
  })
})

describe('fetch helpers', () => {
  it('fetchSprintMembershipSummary populates store and gates by sprintId', async () => {
    const originalFetch = globalThis.fetch
    const responseBody = {
      pbiA: { totalStoryCount: 5, inActiveSprintStoryCount: 2 },
    }
    globalThis.fetch = vi.fn().mockResolvedValue(
      new Response(JSON.stringify(responseBody), { status: 200 }),
    ) as unknown as typeof fetch
    try {
      await useProductWorkspaceStore
        .getState()
        .fetchSprintMembershipSummary('prod-1', 'sprint-A', ['pbiA'])

      const slice = useProductWorkspaceStore.getState().sprintMembership
      expect(slice.pbiSummary.pbiA).toEqual({
        totalStoryCount: 5,
        inActiveSprintStoryCount: 2,
      })
      expect(slice.loadedSummaryForSprintId).toBe('sprint-A')
    } finally {
      globalThis.fetch = originalFetch
    }
  })

  it('fetchCrossSprintBlocks populates store', async () => {
    const originalFetch = globalThis.fetch
    const responseBody = {
      's1': { sprintId: 'sprint-x', sprintName: 'SP-X' },
    }
    globalThis.fetch = vi.fn().mockResolvedValue(
      new Response(JSON.stringify(responseBody), { status: 200 }),
    ) as unknown as typeof fetch
    try {
      await useProductWorkspaceStore
        .getState()
        .fetchCrossSprintBlocks('prod-1', 'sprint-A', ['pbiA'])

      const slice = useProductWorkspaceStore.getState().sprintMembership
      expect(slice.crossSprintBlocks['s1']).toEqual({
        sprintId: 'sprint-x',
        sprintName: 'SP-X',
      })
    } finally {
      globalThis.fetch = originalFetch
    }
  })

  it('fetchSprintMembershipSummary is a no-op for empty pbiIds', async () => {
    const fetchSpy = vi.fn()
    const originalFetch = globalThis.fetch
    globalThis.fetch = fetchSpy as unknown as typeof fetch
    try {
      await useProductWorkspaceStore
        .getState()
        .fetchSprintMembershipSummary('prod-1', 'sprint-A', [])
      expect(fetchSpy).not.toHaveBeenCalled()
    } finally {
      globalThis.fetch = originalFetch
    }
  })
})
