import { describe, expect, test, beforeEach, vi } from 'vitest'
import { syncLegendLayers } from './sync-legend-layers'
import type { LegendSyncStore } from './sync-legend-layers'
import { useLayerStore } from './providers/layers/store'

// ─── A hand-built store that structurally satisfies `LegendSyncStore` ─────────
// Mirrors the react-ui legend store's relevant behavior (idempotent
// setVisibility, always-write setOpacity) without importing react-ui at all.

interface FakeLayer {
  visible: boolean
  opacity: number
  sections?: { id: string; active: string }[]
}

function makeLegendStore(initial: Record<string, FakeLayer>) {
  const listeners = new Set<() => void>()
  const notify = (): void => listeners.forEach((l) => l())

  const state = {
    layers: initial,
    setVisibility(id: string, visible: boolean) {
      const layer = state.layers[id]
      if (!layer || layer.visible === visible) return // idempotent
      state.layers = { ...state.layers, [id]: { ...layer, visible } }
      notify()
    },
    setOpacity(id: string, opacity: number) {
      const layer = state.layers[id]
      if (!layer) return
      state.layers = { ...state.layers, [id]: { ...layer, opacity } }
      notify()
    },
  }

  const store: LegendSyncStore & {
    pickSection: (id: string, sectionId: string, active: string) => void
  } = {
    getState: () => state,
    subscribe: (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
    // Test helper: simulate a config-select pick in the panel.
    pickSection(id, sectionId, active) {
      const layer = state.layers[id]
      if (!layer?.sections) return
      const sections = layer.sections.map((s) =>
        s.id === sectionId ? { ...s, active } : s,
      )
      state.layers = { ...state.layers, [id]: { ...layer, sections } }
      notify()
    },
  }
  return store
}

const mapVisible = (id: string): boolean | undefined =>
  useLayerStore.getState().layers[id]?.props.visible
const mapOpacity = (id: string): number | undefined =>
  useLayerStore.getState().layers[id]?.props.opacity

describe('syncLegendLayers', () => {
  beforeEach(() => {
    useLayerStore.setState({ layers: {} })
  })

  test('first encounter: the map seeds the legend (map authoritative)', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: false, opacity: 0.5 },
    })
    const legend = makeLegendStore({ a: { visible: true, opacity: 1 } })

    const unsub = syncLegendLayers(legend)

    expect(legend.getState().layers.a!.visible).toBe(false)
    expect(legend.getState().layers.a!.opacity).toBe(0.5)
    unsub()
  })

  test('legend → map: visibility and opacity flow to the layer store', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({ a: { visible: true, opacity: 1 } })
    const unsub = syncLegendLayers(legend)

    legend.getState().setVisibility('a', false)
    expect(mapVisible('a')).toBe(false)

    legend.getState().setOpacity('a', 0.3)
    expect(mapOpacity('a')).toBe(0.3)
    unsub()
  })

  test('map → legend: external layer changes reflect in the legend', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({ a: { visible: true, opacity: 1 } })
    const unsub = syncLegendLayers(legend)

    useLayerStore.getState().setLayer('a', { props: { visible: false } })
    expect(legend.getState().layers.a!.visible).toBe(false)

    useLayerStore.getState().setLayer('a', { props: { opacity: 0.2 } })
    expect(legend.getState().layers.a!.opacity).toBe(0.2)
    unsub()
  })

  test('a single legend toggle updates the layer store once (no loop)', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({ a: { visible: true, opacity: 1 } })
    const unsub = syncLegendLayers(legend)

    // Count real layer-store updates: a clean sync writes exactly once; a
    // ping-pong would keep firing.
    let writes = 0
    const unsubscribe = useLayerStore.subscribe(() => {
      writes += 1
    })
    legend.getState().setVisibility('a', false)
    unsubscribe()

    expect(mapVisible('a')).toBe(false)
    expect(writes).toBe(1)
    unsub()
  })

  test('sections are surfaced via onSectionChange (legend→map), seed-silent', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({
      a: { visible: true, opacity: 1, sections: [{ id: 's1', active: 'A' }] },
    })
    const onSectionChange = vi.fn()
    const unsub = syncLegendLayers(legend, { onSectionChange })

    // Seed-silent: no fire for the initial active.
    expect(onSectionChange).not.toHaveBeenCalled()

    legend.pickSection('a', 's1', 'B')
    expect(onSectionChange).toHaveBeenCalledTimes(1)
    expect(onSectionChange).toHaveBeenCalledWith({
      layerId: 'a',
      sectionId: 's1',
      value: 'B',
    })
    unsub()
  })

  test('sections never write back to the layer store', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({
      a: { visible: true, opacity: 1, sections: [{ id: 's1', active: 'A' }] },
    })
    const unsub = syncLegendLayers(legend, { onSectionChange: vi.fn() })

    // A section pick must not touch the layer store — its entry stays identical.
    const before = useLayerStore.getState().layers.a
    legend.pickSection('a', 's1', 'B')

    expect(useLayerStore.getState().layers.a).toBe(before)
    unsub()
  })

  test('ids present on only one side are skipped', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    // `b` exists only in the legend; `a` only in the map gets synced normally.
    const legend = makeLegendStore({
      a: { visible: true, opacity: 1 },
      b: { visible: true, opacity: 1 },
    })

    const unsub = syncLegendLayers(legend)
    legend.getState().setVisibility('b', false)
    // No map layer `b` to write to — nothing throws, `a` is unaffected.
    expect(useLayerStore.getState().layers.b).toBeUndefined()
    expect(mapVisible('a')).toBe(true)
    unsub()
  })

  test('teardown: after unsubscribe, changes no longer propagate', () => {
    useLayerStore.getState().setLayer('a', {
      type: null,
      props: { id: 'a', visible: true, opacity: 1 },
    })
    const legend = makeLegendStore({ a: { visible: true, opacity: 1 } })
    const unsub = syncLegendLayers(legend)

    legend.getState().setVisibility('a', false)
    expect(mapVisible('a')).toBe(false) // still subscribed

    unsub()
    legend.getState().setVisibility('a', true)
    expect(mapVisible('a')).toBe(false) // detached — map not updated
  })
})
