import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getCheckInTask, getCheckInTasks } from '../src/data/check-in'
import { getPoiRepository } from '../src/data/poi'
import {
  applyCheckIn,
  CHECK_IN_BADGES,
  CHECK_IN_MAX_ACCURACY_METERS,
  CHECK_IN_RADIUS_METERS,
  type CheckInLocationSnapshot,
  createCheckInRecordId,
  createEmptyCheckInProfile,
  distanceBetweenMeters,
  evaluateCheckInEligibility,
  normalizeCheckInProfile,
} from '../src/domain/check-in'
import {
  CHECK_IN_STORAGE_KEY,
  checkInAtPoi,
  clearCheckInProfile,
  loadCheckInProfile,
  saveCheckInProfile,
} from '../src/services/check-in'

const poi = {
  id: 'poi_hongqiao_park',
  name: '虹桥公园',
  longitude: 113.944,
  latitude: 22.755,
}

function location(overrides: Partial<CheckInLocationSnapshot> = {}): CheckInLocationSnapshot {
  return {
    longitude: poi.longitude,
    latitude: poi.latitude,
    accuracy: 20,
    receivedAt: '2026-07-31T02:00:00.000Z',
    coordinateSystem: 'GCJ02',
    ...overrides,
  }
}

describe('check-in domain', () => {
  it('exposes every published POI through the task catalogue used by detail pages', () => {
    const publishedPoiIds = getPoiRepository().getPublishedPois().map(item => item.id)
    const tasks = getCheckInTasks()

    expect(tasks.map(task => task.poiId)).toEqual(publishedPoiIds)
    expect(tasks).toHaveLength(publishedPoiIds.length)
    expect(tasks.every(task => task.radiusMeters === CHECK_IN_RADIUS_METERS)).toBe(true)
    expect(publishedPoiIds.every(poiId => getCheckInTask(poiId)?.poiId === poiId)).toBe(true)
    expect(getCheckInTask('poi_not_published')).toBeNull()
  })

  it('computes short GCJ-02 distances in metres', () => {
    expect(distanceBetweenMeters(poi, poi)).toBe(0)
    expect(distanceBetweenMeters(poi, { ...poi, latitude: poi.latitude + 0.001 })).toBeCloseTo(111, 0)
  })

  it('requires an accurate location inside the check-in radius', () => {
    expect(evaluateCheckInEligibility(location(), poi)).toMatchObject({ eligible: true, code: 'eligible' })
    expect(evaluateCheckInEligibility(location({ latitude: poi.latitude + 0.003 }), poi)).toMatchObject({
      eligible: false,
      code: 'too_far',
      allowedDistanceMeters: CHECK_IN_RADIUS_METERS,
    })
    expect(evaluateCheckInEligibility(location({ latitude: poi.latitude + 0.0017, accuracy: 20 }), poi).code)
      .toBe('uncertain')
    expect(evaluateCheckInEligibility(location({ accuracy: 0 }), poi).code).toBe('low_accuracy')
    expect(evaluateCheckInEligibility(location({ accuracy: CHECK_IN_MAX_ACCURACY_METERS + 1 }), poi).code)
      .toBe('low_accuracy')
  })

  it('applies conservative distance and accuracy boundary comparisons', () => {
    const offsetLocation = location({ latitude: poi.latitude + 0.001, accuracy: 20 })
    const distance = distanceBetweenMeters(offsetLocation, poi)

    expect(evaluateCheckInEligibility(offsetLocation, poi, distance + 20).code).toBe('eligible')
    expect(evaluateCheckInEligibility(offsetLocation, poi, distance + 20 - 0.001).code).toBe('uncertain')
    expect(evaluateCheckInEligibility(offsetLocation, poi, distance - 20).code).toBe('uncertain')
    expect(evaluateCheckInEligibility(offsetLocation, poi, distance - 20 - 0.001).code).toBe('too_far')
    expect(evaluateCheckInEligibility(location({ accuracy: CHECK_IN_MAX_ACCURACY_METERS }), poi).code)
      .toBe('eligible')
  })

  it('awards points and threshold badges without persisting exact coordinates', () => {
    const now = Date.parse('2026-07-31T02:00:20.000Z')
    const result = applyCheckIn(createEmptyCheckInProfile(), poi, location(), now)

    expect(result.profile.points).toBe(10)
    expect(result.profile.badges.map(badge => badge.code)).toEqual(['explorer'])
    expect(result.record).toMatchObject({ poiId: poi.id, pointsAwarded: 10 })
    expect(JSON.stringify(result.profile)).not.toContain(String(poi.longitude))
    expect(() => applyCheckIn(result.profile, poi, location(), now + 1000)).toThrow('不会重复计分')
  })

  it('keeps the backwards-compatible 1/2/3 cumulative badge thresholds', () => {
    expect(CHECK_IN_BADGES.map(badge => badge.requiredCheckIns)).toEqual([1, 2, 3])

    let profile = createEmptyCheckInProfile()
    for (let index = 0; index < 3; index += 1) {
      const result = applyCheckIn(
        profile,
        { ...poi, id: `poi_${index}`, name: `点位 ${index}` },
        location(),
        Date.parse('2026-07-31T02:00:20.000Z') + index,
      )
      profile = result.profile
    }
    expect(profile.badges.map(badge => badge.code)).toEqual(['explorer', 'traveller', 'check_in_master'])
  })

  it('rejects malformed profiles and inconsistent point totals', () => {
    const result = applyCheckIn(
      createEmptyCheckInProfile(),
      poi,
      location(),
      Date.parse('2026-07-31T02:00:20.000Z'),
    )
    expect(normalizeCheckInProfile({ ...result.profile, points: 999 })).toBeNull()
    expect(normalizeCheckInProfile({ ...result.profile, badges: [...result.profile.badges, result.profile.badges[0]] })).toBeNull()
    expect(normalizeCheckInProfile({
      ...result.profile,
      checkIns: [{ ...result.profile.checkIns[0], longitude: 113.94 }],
    })).toBeNull()
    expect(normalizeCheckInProfile({ ...result.profile, location: location() })).toBeNull()
  })
})

describe('check-in storage', () => {
  const values = new Map<string, unknown>()

  beforeEach(() => {
    values.clear()
    vi.mocked(uni.getStorageSync).mockImplementation(key => values.get(key))
    vi.mocked(uni.setStorageSync).mockImplementation((key, value) => values.set(key, value))
    vi.mocked(uni.removeStorageSync).mockImplementation(key => values.delete(key))
  })

  it('allows the service gate to check in at every published POI', () => {
    const publishedPois = getPoiRepository().getPublishedPois()

    publishedPois.forEach((publishedPoi, index) => {
      values.clear()
      const result = checkInAtPoi(
        publishedPoi.id,
        location({
          longitude: publishedPoi.longitude,
          latitude: publishedPoi.latitude,
        }),
        Date.parse('2026-07-31T02:00:20.000Z') + index,
      )

      expect(result.record.poiId).toBe(publishedPoi.id)
      expect(loadCheckInProfile().checkIns[0].poiId).toBe(publishedPoi.id)
    })
  })

  it('rejects an unknown or retired POI at the service gate', () => {
    const publishedPoi = getPoiRepository().getPublishedPois()[0]

    expect(() => checkInAtPoi(
      'poi_not_published',
      location({ longitude: publishedPoi.longitude, latitude: publishedPoi.latitude }),
    )).toThrow('不存在或已下线')
    expect(values.has(CHECK_IN_STORAGE_KEY)).toBe(false)
  })

  it('uses canonical repository coordinates at the service boundary', () => {
    const publishedPoi = getPoiRepository().getPublishedPois()[0]

    expect(() => checkInAtPoi(
      publishedPoi.id,
      location({ longitude: publishedPoi.longitude + 0.01, latitude: publishedPoi.latitude }),
    )).toThrow('请到点位')
    expect(values.has(CHECK_IN_STORAGE_KEY)).toBe(false)
  })

  it('round-trips only the dedicated anonymous profile key', () => {
    const result = applyCheckIn(
      createEmptyCheckInProfile(),
      poi,
      location(),
      Date.parse('2026-07-31T02:00:20.000Z'),
    )
    saveCheckInProfile(result.profile)

    const loaded = loadCheckInProfile()
    loaded.checkIns[0].poiName = 'changed'
    expect(loadCheckInProfile().checkIns[0].poiName).toBe('虹桥公园')
    expect([...values.keys()]).toEqual([CHECK_IN_STORAGE_KEY])
    expect(JSON.stringify(values.get(CHECK_IN_STORAGE_KEY))).not.toContain(String(poi.longitude))
  })

  it('fails closed for malformed reads and clears only check-in data', () => {
    values.set(CHECK_IN_STORAGE_KEY, { broken: true })
    values.set('guangming:last-plan', { keep: true })
    expect(() => loadCheckInProfile()).toThrow('本机打卡记录异常')

    clearCheckInProfile()
    expect(values.has(CHECK_IN_STORAGE_KEY)).toBe(false)
    expect(values.has('guangming:last-plan')).toBe(true)
  })

  it('rejects unknown storage, envelope, and badge fields', () => {
    const savedAt = '2026-07-31T02:00:20.000Z'
    values.set(CHECK_IN_STORAGE_KEY, { schemaVersion: 99, savedAt, payload: createEmptyCheckInProfile() })
    expect(() => loadCheckInProfile()).toThrow('本机打卡记录异常')

    values.set(CHECK_IN_STORAGE_KEY, {
      schemaVersion: 2,
      savedAt,
      payload: createEmptyCheckInProfile(),
      longitude: 113.94,
    })
    expect(() => loadCheckInProfile()).toThrow('本机打卡记录异常')

    const checkedInAt = savedAt
    values.set(CHECK_IN_STORAGE_KEY, {
      schemaVersion: 2,
      savedAt,
      payload: {
        points: 10,
        checkIns: [{
          id: createCheckInRecordId(poi.id, checkedInAt),
          poiId: poi.id,
          poiName: poi.name,
          checkedInAt,
          pointsAwarded: 10,
        }],
        badges: [{ code: 'explorer', obtainedAt: checkedInAt, accuracy: 20 }],
      },
    })
    expect(() => loadCheckInProfile()).toThrow('本机打卡记录异常')
  })

  it('preserves valid v2 history when a POI is no longer enabled', () => {
    const checkedInAt = '2026-07-31T02:00:20.000Z'
    const legacyPoiId = 'poi_legacy_hangzhou'
    values.set(CHECK_IN_STORAGE_KEY, {
      schemaVersion: 2,
      savedAt: checkedInAt,
      payload: {
        points: 10,
        checkIns: [{
          id: createCheckInRecordId(legacyPoiId, checkedInAt),
          poiId: legacyPoiId,
          poiName: '旧点位',
          checkedInAt,
          pointsAwarded: 10,
        }],
        badges: [{ code: 'explorer', obtainedAt: checkedInAt }],
      },
    })

    const profile = {
      points: 10,
      badges: [{ code: 'explorer', obtainedAt: checkedInAt }],
      checkIns: [{
        id: createCheckInRecordId(legacyPoiId, checkedInAt),
        poiId: legacyPoiId,
        poiName: '旧点位',
        checkedInAt,
        pointsAwarded: 10,
      }],
    } as const
    expect(loadCheckInProfile()).toEqual(profile)
    expect(() => saveCheckInProfile(profile)).not.toThrow()
  })

  it('migrates legacy data to the current published POIs and badge rules', () => {
    const firstTime = '2026-07-31T02:00:20.000Z'
    const secondTime = '2026-07-31T03:00:20.000Z'
    values.set(CHECK_IN_STORAGE_KEY, {
      schemaVersion: 1,
      savedAt: secondTime,
      payload: {
        points: 30,
        badges: [{ code: 'explorer', obtainedAt: firstTime }],
        checkIns: [
          { poiId: 'poi_hongqiao_park', poiName: '虹桥公园', checkedInAt: firstTime },
          { poiId: 'poi_gm_culture_center', poiName: '光明文化艺术中心', checkedInAt: secondTime },
          { poiId: 'poi_legacy_hangzhou', poiName: '旧点位', checkedInAt: secondTime },
        ],
      },
    })

    const migrated = loadCheckInProfile()
    expect(migrated.points).toBe(20)
    expect(migrated.checkIns.map(record => record.poiId)).toEqual([
      'poi_gm_culture_center',
      'poi_hongqiao_park',
    ])
    expect(migrated.badges.map(badge => badge.code)).toEqual(['explorer', 'traveller'])
    expect(values.get(CHECK_IN_STORAGE_KEY)).toMatchObject({ schemaVersion: 2, payload: migrated })
  })

  it('does not report success when the atomic profile write fails', () => {
    const result = applyCheckIn(
      createEmptyCheckInProfile(),
      poi,
      location(),
      Date.parse('2026-07-31T02:00:20.000Z'),
    )
    vi.mocked(uni.setStorageSync).mockImplementation(() => {
      throw new Error('quota exceeded')
    })

    expect(() => saveCheckInProfile(result.profile)).toThrow('无法保存本机打卡记录：quota exceeded')
    expect(values.has(CHECK_IN_STORAGE_KEY)).toBe(false)
  })
})
