import { describe, expect, it, vi } from 'vitest' import type { MuseumFloor, MuseumPoi } from '@/domain/museum' import type { PoiCategoryId } from '@/domain/poiCategories' import type { GuideRepository } from '@/repositories/GuideRepository' import type { GuideModelRepository } from '@/repositories/GuideModelRepository' import { GuideUseCase } from '@/usecases/guideUseCase' const floors: MuseumFloor[] = [ { id: 'L1', label: '1F', order: 1 }, { id: 'L2', label: '2F', order: 2 } ] const quickCategoryIds: readonly PoiCategoryId[] = [ 'exhibition-hall', 'cinema', 'ticket-office', 'dining', 'shopping', 'service-center', 'restroom', 'nursing-room', 'elevator', 'escalator' ] const createPoi = (id: string, floorId: string, categoryId: string): MuseumPoi => ({ id, name: id, floorId, floorLabel: floorId === 'L2' ? '2F' : '1F', primaryCategory: { id: categoryId, label: categoryId, iconType: categoryId }, categories: [], accessible: true }) const destinationsByFloor: Record = { L1: [createPoi('space-l1-hall', 'L1', 'exhibition_hall')], L2: [createPoi('space-l2-hall', 'L2', 'exhibition_hall')] } const quickResultsByFloor: Record>> = { L1: { restroom: [createPoi('facility-l1-restroom', 'L1', 'toilet')] }, L2: { restroom: [createPoi('facility-l2-restroom', 'L2', 'toilet')] } } const keywordResultsByFloor: Record = { L1: [createPoi('facility-l1-shop', 'L1', 'shop')], L2: [createPoi('facility-l2-shop', 'L2', 'shop')] } const availability = quickCategoryIds.map((id, index) => ({ id, count: index === quickCategoryIds.indexOf('restroom') ? 1 : 0, disabled: id !== 'restroom' })) const createRepository = () => ({ getAssetBaseUrl: vi.fn(() => ''), getFloors: vi.fn().mockResolvedValue(floors), normalizeFloorId: vi.fn((floorId: string) => ({ '1F': 'L1', '2F': 'L2' }[floorId] || floorId)), listPois: vi.fn().mockResolvedValue([]), listSpacePoints: vi.fn().mockResolvedValue([]), listDestinationPois: vi.fn((floorId?: string) => ( Promise.resolve(destinationsByFloor[floorId || 'L1'] || []) )), listQuickFindPois: vi.fn((categoryId: PoiCategoryId, floorId?: string) => ( Promise.resolve(quickResultsByFloor[floorId || 'L1']?.[categoryId] || []) )), getQuickFindCategoryAvailability: vi.fn().mockResolvedValue(availability), getPoiById: vi.fn().mockResolvedValue(null), searchPois: vi.fn((keyword?: string, floorId?: string) => ( Promise.resolve(keyword ? keywordResultsByFloor[floorId || 'L1'] || [] : []) )), getLocationPreview: vi.fn().mockResolvedValue(null), getRouteReadiness: vi.fn().mockResolvedValue({}) }) const expectMapListParity = (state: { results: MuseumPoi[], visiblePoiIds: string[] }) => { expect(state.visiblePoiIds).toEqual(state.results.map((poi) => poi.id)) } describe('GuideUseCase point search state', () => { it('loads initial destinations through listDestinationPois and never searchPois', async () => { const repository = createRepository() const useCase = new GuideUseCase( repository as unknown as GuideRepository, {} as GuideModelRepository ) await expect(useCase.getInitialSearchSpacePoints('L2')) .resolves.toEqual(destinationsByFloor.L2) expect(repository.listDestinationPois).toHaveBeenCalledWith('L2') expect(repository.searchPois).not.toHaveBeenCalled() }) it('creates the default state from current-floor destinations and relays all ten availability entries', async () => { const repository = createRepository() const useCase = new GuideUseCase( repository as unknown as GuideRepository, {} as GuideModelRepository ) const state = await useCase.createInitialPoiSearchState('L1') expect(state).toMatchObject({ mode: 'default', floorId: 'L1', floorLabel: '1F', keyword: '', categoryId: '' }) expect(state.results).toEqual(destinationsByFloor.L1) expectMapListParity(state) expect(state.categories.map((item) => ({ id: item.definition.id, count: item.count, disabled: item.disabled }))).toEqual(availability) expect(repository.getQuickFindCategoryAvailability).toHaveBeenCalledWith('L1') expect(repository.listDestinationPois).toHaveBeenCalledWith('L1') expect(repository.searchPois).not.toHaveBeenCalled() }) it('keeps category, keyword, clear, and floor-change states on canonical result IDs', async () => { const repository = createRepository() const useCase = new GuideUseCase( repository as unknown as GuideRepository, {} as GuideModelRepository ) const initial = await useCase.createInitialPoiSearchState('L1') const category = await useCase.selectPoiSearchCategory(initial, 'restroom') expect(category).toMatchObject({ mode: 'category', floorId: 'L1', categoryId: 'restroom' }) expect(category.results).toEqual(quickResultsByFloor.L1.restroom) expectMapListParity(category) expect(repository.listQuickFindPois).toHaveBeenCalledWith('restroom', 'L1') expect(repository.searchPois).not.toHaveBeenCalled() const keyword = await useCase.searchPoiKeyword(category, 'shop') expect(keyword).toMatchObject({ mode: 'keyword', floorId: 'L1', keyword: 'shop', categoryId: '' }) expect(keyword.results).toEqual(keywordResultsByFloor.L1) expectMapListParity(keyword) expect(repository.searchPois).toHaveBeenLastCalledWith('shop', 'L1') const switchedFloor = await useCase.changePoiSearchFloor(keyword, 'L2') expect(switchedFloor).toMatchObject({ mode: 'keyword', floorId: 'L2', floorLabel: '2F', keyword: 'shop', categoryId: '' }) expect(switchedFloor.results).toEqual(keywordResultsByFloor.L2) expectMapListParity(switchedFloor) expect(repository.searchPois).toHaveBeenLastCalledWith('shop', 'L2') const cleared = await useCase.clearPoiSearch(switchedFloor) expect(cleared).toMatchObject({ mode: 'default', floorId: 'L2', floorLabel: '2F', keyword: '', categoryId: '' }) expect(cleared.results).toEqual(destinationsByFloor.L2) expectMapListParity(cleared) expect(repository.listDestinationPois).toHaveBeenLastCalledWith('L2') }) })