import { createComponent, createEffect, createRoot, useContext } from 'solid-js'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { MAX_ACTIVE_PLUGINS } from '../utils/constants'
import { flattenTabs } from '../utils/layout-tree'
import { TANSTACK_DEVTOOLS_STATE } from '../utils/storage'
import {
  DevtoolsContext,
  DevtoolsProvider,
  getExistingStateFromStorage,
  getStateFromLocalStorage,
} from './devtools-context'
import type { TanStackDevtoolsPlugin } from './devtools-context'

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

describe('getStateFromLocalStorage', () => {
  beforeEach(() => {
    localStorage.clear()
  })
  it('should return undefined when no data in localStorage', () => {
    const state = getStateFromLocalStorage(undefined)
    expect(state).toEqual(undefined)
  })
  it('migrates a legacy activePlugins array into a single layout group', () => {
    const mockState = {
      activePlugins: ['plugin1'],
      settings: {
        theme: 'dark',
      },
    }
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))
    const state = getStateFromLocalStorage([
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
      },
    ])
    // State written before the workspace became a tree reopens as one group, in
    // the stored order, and the superseded key is dropped.
    expect(state).toEqual({
      layout: { kind: 'group', id: 'g0', tabs: ['plugin1'], active: 0 },
      settings: { theme: 'dark' },
    })
    expect(state).not.toHaveProperty('activePlugins')
    // The migration is written back, so it only happens once.
    expect(
      JSON.parse(localStorage.getItem(TANSTACK_DEVTOOLS_STATE)!),
    ).not.toHaveProperty('activePlugins')
  })

  it('keeps an existing layout tree instead of rebuilding it', () => {
    const layout = {
      kind: 'split',
      dir: 'row',
      sizes: [0.5, 0.5],
      children: [
        { kind: 'group', id: 'g0', tabs: ['plugin1'], active: 0 },
        { kind: 'group', id: 'g1', tabs: ['plugin2'], active: 0 },
      ],
    }
    localStorage.setItem(
      TANSTACK_DEVTOOLS_STATE,
      JSON.stringify({ layout, activePlugins: ['plugin1'] }),
    )
    const state = getStateFromLocalStorage([
      { id: 'plugin1', render: () => {}, name: 'Plugin 1' },
      { id: 'plugin2', render: () => {}, name: 'Plugin 2' },
    ])
    // The tree wins: activePlugins is the superseded key, not a second opinion.
    expect(state?.layout).toEqual(layout)
    expect(flattenTabs(state?.layout ?? null)).toEqual(['plugin1', 'plugin2'])
  })
  it('should filter out inactive plugins', () => {
    const mockState = {
      activePlugins: ['plugin1', 'plugin2'],
      settings: {
        theme: 'dark',
      },
    }
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))
    const plugins = [{ id: 'plugin1', render: () => {}, name: 'Plugin 1' }]
    const state = getStateFromLocalStorage(plugins)
    expect(flattenTabs(state?.layout ?? null)).toEqual(['plugin1'])
  })
  it('should return empty plugin state if all active plugins are invalid', () => {
    const mockState = {
      activePlugins: ['plugin1', 'plugin2'],
      settings: {
        theme: 'dark',
      },
    }
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))
    const plugins = [{ id: 'plugin3', render: () => {}, name: 'Plugin 3' }]
    const state = getStateFromLocalStorage(plugins)
    expect(flattenTabs(state?.layout ?? null)).toEqual([])
  })
  it('should handle invalid JSON in localStorage gracefully', () => {
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, 'invalid json')
    const state = getStateFromLocalStorage(undefined)
    expect(state).toEqual(undefined)
  })

  it('should return undefined when no localStorage state exists (allowing defaultOpen to be applied)', () => {
    // No existing state in localStorage - this allows defaultOpen logic to trigger
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
        defaultOpen: true,
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
        defaultOpen: false,
      },
      {
        id: 'plugin3',
        render: () => {},
        name: 'Plugin 3',
        defaultOpen: true,
      },
    ]

    // When undefined is returned, getExistingStateFromStorage will fill activePlugins with defaultOpen plugins
    const state = getStateFromLocalStorage(plugins)
    expect(state).toEqual(undefined)
  })

  it('should preserve existing activePlugins from localStorage (defaultOpen should not override)', () => {
    const mockState = {
      activePlugins: ['plugin2'],
      settings: {
        theme: 'dark',
      },
    }
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))

    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
        defaultOpen: true,
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
        defaultOpen: false,
      },
    ]

    const state = getStateFromLocalStorage(plugins)
    // Should keep existing activePlugins - defaultOpen logic won't override in getExistingStateFromStorage
    expect(flattenTabs(state?.layout ?? null)).toEqual(['plugin2'])
  })

  it('should automatically activate a single plugin when no active plugins exist', () => {
    // No existing state in localStorage
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'only-plugin',
        render: () => {},
        name: 'Only Plugin',
      },
    ]

    const state = getStateFromLocalStorage(plugins)
    // Should return undefined - the single plugin activation happens in getExistingStateFromStorage
    expect(state).toEqual(undefined)
  })
})

describe('getExistingStateFromStorage - integration tests', () => {
  beforeEach(() => {
    localStorage.clear()
  })

  it('should automatically activate a single plugin when no localStorage state exists', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'only-plugin',
        render: () => {},
        name: 'Only Plugin',
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    expect(flattenTabs(state.state.layout)).toEqual(['only-plugin'])
    expect(state.plugins).toHaveLength(1)
    expect(state.plugins![0]?.id).toBe('only-plugin')
  })

  it('should activate plugins with defaultOpen: true when no localStorage state exists', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
        defaultOpen: true,
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
        defaultOpen: false,
      },
      {
        id: 'plugin3',
        render: () => {},
        name: 'Plugin 3',
        defaultOpen: true,
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    expect(flattenTabs(state.state.layout)).toEqual(['plugin1', 'plugin3'])
    expect(state.plugins).toHaveLength(3)
  })

  it('opens every defaultOpen plugin that fits under MAX_ACTIVE_PLUGINS', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
        defaultOpen: true,
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
        defaultOpen: true,
      },
      {
        id: 'plugin3',
        render: () => {},
        name: 'Plugin 3',
        defaultOpen: true,
      },
      {
        id: 'plugin4',
        render: () => {},
        name: 'Plugin 4',
        defaultOpen: true,
      },
      {
        id: 'plugin5',
        render: () => {},
        name: 'Plugin 5',
        defaultOpen: true,
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    // Five is under the cap now that panes can split and stack, so all five
    // open. Pinned to the constant rather than a literal.
    expect(flattenTabs(state.state.layout)).toEqual(
      plugins.slice(0, MAX_ACTIVE_PLUGINS).map((plugin) => plugin.id),
    )
    expect(flattenTabs(state.state.layout).length).toBeLessThanOrEqual(
      MAX_ACTIVE_PLUGINS,
    )
    // All 5 plugins should still be in the plugins array
    expect(state.plugins).toHaveLength(5)
  })

  it('should preserve existing activePlugins from localStorage even when plugins have defaultOpen', () => {
    const mockState = {
      activePlugins: ['plugin2', 'plugin4'],
      settings: {
        theme: 'dark',
      },
    }
    localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))

    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
        defaultOpen: true,
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
        defaultOpen: false,
      },
      {
        id: 'plugin3',
        render: () => {},
        name: 'Plugin 3',
        defaultOpen: true,
      },
      {
        id: 'plugin4',
        render: () => {},
        name: 'Plugin 4',
        defaultOpen: false,
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    // Should preserve the localStorage state, not use defaultOpen
    expect(flattenTabs(state.state.layout)).toEqual(['plugin2', 'plugin4'])
    expect(state.plugins).toHaveLength(4)
  })

  it('should return empty activePlugins when no defaultOpen and multiple plugins', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
      },
      {
        id: 'plugin2',
        render: () => {},
        name: 'Plugin 2',
      },
      {
        id: 'plugin3',
        render: () => {},
        name: 'Plugin 3',
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    expect(flattenTabs(state.state.layout)).toEqual([])
    expect(state.plugins).toHaveLength(3)
  })

  it('should handle single plugin with defaultOpen: false by activating it anyway', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'only-plugin',
        render: () => {},
        name: 'Only Plugin',
        defaultOpen: false,
      },
    ]

    const state = getExistingStateFromStorage(undefined, plugins)
    // Single plugin should be activated regardless of defaultOpen flag
    expect(flattenTabs(state.state.layout)).toEqual(['only-plugin'])
  })

  it('should merge config settings into the returned state', () => {
    const plugins: Array<TanStackDevtoolsPlugin> = [
      {
        id: 'plugin1',
        render: () => {},
        name: 'Plugin 1',
      },
    ]

    const config = {
      theme: 'light' as const,
    }

    const state = getExistingStateFromStorage(config as any, plugins)
    expect(state.settings.theme).toBe('light')
    expect(flattenTabs(state.state.layout)).toEqual(['plugin1'])
  })
})

describe('provider resilience', () => {
  beforeEach(() => localStorage.clear())

  it('fails loudly for storage reads while tolerating storage quota errors', () => {
    vi.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() => {
      throw new Error('read blocked')
    })
    expect(() => getStateFromLocalStorage(undefined)).toThrow('read blocked')
    localStorage.setItem(
      TANSTACK_DEVTOOLS_STATE,
      JSON.stringify({
        activePlugins: ['missing'],
        settings: { theme: 'dark' },
      }),
    )
    vi.spyOn(Storage.prototype, 'setItem').mockImplementationOnce(() => {
      throw new Error('quota')
    })
    expect(() =>
      getStateFromLocalStorage([
        { id: 'kept', name: 'Kept', render: () => {} },
      ]),
    ).not.toThrow()
  })

  it('generates stable unique IDs while preserving explicit duplicate IDs', () => {
    const generated = getExistingStateFromStorage(undefined, [
      { name: 'Same Name', render: () => {} },
      { name: 'Same Name', render: () => {} },
      { name: () => {}, render: () => {} },
    ]).plugins!.map((entry) => entry.id)
    expect(generated).toEqual(['same-name-0', 'same-name-1', '2'])
    const explicit = getExistingStateFromStorage(undefined, [
      { id: 'duplicate', name: 'A', render: () => {} },
      { id: 'duplicate', name: 'B', render: () => {} },
    ]).plugins!.map((entry) => entry.id)
    expect(explicit).toEqual(['duplicate', 'duplicate'])
  })

  it('reactively replaces plugins through the existing onSetPlugins callback', async () => {
    let replace!: (plugins: Array<TanStackDevtoolsPlugin>) => void
    let ids: Array<string> = []
    const dispose = createRoot((disposeRoot) => {
      createComponent(DevtoolsProvider, {
        plugins: [{ name: 'Old', render: () => {} }],
        onSetPlugins: (setter) => {
          replace = setter
        },
        get children() {
          const context = useContext(DevtoolsContext)!
          createEffect(() => {
            ids = context.store.plugins?.map((entry) => entry.id!) ?? []
          })
          return null
        },
      })
      return disposeRoot
    })
    await Promise.resolve()
    expect(ids).toEqual(['old-0'])
    replace([{ name: 'New', render: () => {} }])
    expect(ids).toEqual(['new-0'])
    dispose()
  })
})
