import { using } from '@furystack/utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { LAYOUT_CSS_VARIABLES, createLayoutService } from './layout-service.js'

describe('LayoutService', () => {
  let mockSetProperty: ReturnType<typeof vi.fn>
  let mockElement: { style: { setProperty: ReturnType<typeof vi.fn> } }

  beforeEach(() => {
    mockSetProperty = vi.fn()
    mockElement = {
      style: {
        setProperty: mockSetProperty,
      },
    }
  })

  afterEach(() => {
    vi.unstubAllGlobals()
  })

  describe('Constructor', () => {
    it('should initialize with default values', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        expect(service.drawerState.getValue()).toEqual({})
        expect(service.appBarVisible.getValue()).toBe(true)
        expect(service.appBarHeight.getValue()).toBe('48px')
        expect(service.topGap.getValue()).toBe('0px')
        expect(service.sideGap.getValue()).toBe('0px')
      })
    })

    it('should update CSS variables on initialization when element is provided', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), () => {
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-appbar-height', '48px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-top-gap', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-side-gap', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-padding-top', 'calc(48px + 0px)')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-top', '48px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-width', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-configured-width', '0px')
      })
    })

    it('should not throw when element is undefined', () => {
      expect(() => {
        using(createLayoutService(undefined), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          service.setTopGap('16px')
          service.setSideGap('24px')
        })
      }).not.toThrow()
    })
  })

  describe('Drawer State Management', () => {
    describe('toggleDrawer', () => {
      it('should toggle drawer from closed to open', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: false, width: '240px', variant: 'collapsible' })

          service.toggleDrawer('left')

          expect(service.drawerState.getValue().left?.open).toBe(true)
        })
      })

      it('should toggle drawer from open to closed', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })

          service.toggleDrawer('left')

          expect(service.drawerState.getValue().left?.open).toBe(false)
        })
      })

      it('should do nothing if drawer is not initialized', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.toggleDrawer('left')

          expect(service.drawerState.getValue().left).toBeUndefined()
        })
      })

      it('should toggle right drawer independently', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          service.initDrawer('right', { open: false, width: '200px', variant: 'temporary' })

          service.toggleDrawer('right')

          expect(service.drawerState.getValue().left?.open).toBe(true)
          expect(service.drawerState.getValue().right?.open).toBe(true)
        })
      })
    })

    describe('setDrawerOpen', () => {
      it('should set drawer open state', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setDrawerOpen('left', true)

          expect(service.drawerState.getValue().left?.open).toBe(true)
        })
      })

      it('should create drawer entry with default width if not exists', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setDrawerOpen('left', true)

          expect(service.drawerState.getValue().left?.width).toBe('240px')
        })
      })

      it('should create drawer entry with default variant if not exists', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setDrawerOpen('left', true)

          expect(service.drawerState.getValue().left?.variant).toBe('collapsible')
        })
      })

      it('should preserve existing width when setting open state', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: false, width: '300px', variant: 'collapsible' })

          service.setDrawerOpen('left', true)

          expect(service.drawerState.getValue().left?.width).toBe('300px')
        })
      })

      it('should preserve existing variant when setting open state', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: false, width: '240px', variant: 'temporary' })

          service.setDrawerOpen('left', true)

          expect(service.drawerState.getValue().left?.variant).toBe('temporary')
        })
      })
    })

    describe('setDrawerWidth', () => {
      it('should set drawer width', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })

          service.setDrawerWidth('left', '300px')

          expect(service.drawerState.getValue().left?.width).toBe('300px')
        })
      })

      it('should create drawer entry with default closed state if not exists', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setDrawerWidth('left', '300px')

          expect(service.drawerState.getValue().left?.open).toBe(false)
        })
      })

      it('should create drawer entry with default variant if not exists', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setDrawerWidth('left', '300px')

          expect(service.drawerState.getValue().left?.variant).toBe('collapsible')
        })
      })

      it('should preserve open state when setting width', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })

          service.setDrawerWidth('left', '300px')

          expect(service.drawerState.getValue().left?.open).toBe(true)
        })
      })

      it('should preserve variant when setting width', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'permanent' })

          service.setDrawerWidth('left', '300px')

          expect(service.drawerState.getValue().left?.variant).toBe('permanent')
        })
      })
    })

    describe('initDrawer', () => {
      it('should initialize left drawer', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '250px', variant: 'collapsible' })

          expect(service.drawerState.getValue().left).toEqual({ open: true, width: '250px', variant: 'collapsible' })
        })
      })

      it('should initialize right drawer', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('right', { open: false, width: '200px', variant: 'temporary' })

          expect(service.drawerState.getValue().right).toEqual({ open: false, width: '200px', variant: 'temporary' })
        })
      })

      it('should initialize both drawers', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'permanent' })
          service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })

          expect(service.drawerState.getValue()).toEqual({
            left: { open: true, width: '240px', variant: 'permanent' },
            right: { open: true, width: '200px', variant: 'temporary' },
          })
        })
      })

      it('should overwrite existing drawer config', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          service.initDrawer('left', { open: false, width: '300px', variant: 'permanent' })

          expect(service.drawerState.getValue().left).toEqual({ open: false, width: '300px', variant: 'permanent' })
        })
      })

      it('should initialize drawer with all variant types', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'permanent' })
          expect(service.drawerState.getValue().left?.variant).toBe('permanent')

          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          expect(service.drawerState.getValue().left?.variant).toBe('collapsible')

          service.initDrawer('left', { open: true, width: '240px', variant: 'temporary' })
          expect(service.drawerState.getValue().left?.variant).toBe('temporary')
        })
      })
    })

    describe('removeDrawer', () => {
      it('should remove left drawer state', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })

          service.removeDrawer('left')

          expect(service.drawerState.getValue().left).toBeUndefined()
        })
      })

      it('should remove right drawer state', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })

          service.removeDrawer('right')

          expect(service.drawerState.getValue().right).toBeUndefined()
        })
      })

      it('should not affect the other drawer when removing one', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })

          service.removeDrawer('left')

          expect(service.drawerState.getValue().left).toBeUndefined()
          expect(service.drawerState.getValue().right).toEqual({ open: true, width: '200px', variant: 'temporary' })
        })
      })

      it('should be a no-op if the drawer does not exist', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          const stateBefore = service.drawerState.getValue()

          service.removeDrawer('left')

          expect(service.drawerState.getValue()).toEqual(stateBefore)
        })
      })

      it('should be a no-op if the service is already disposed', () => {
        const service = createLayoutService(mockElement as unknown as HTMLElement)
        service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
        service[Symbol.dispose]()

        expect(() => service.removeDrawer('left')).not.toThrow()
      })

      it('should reset CSS variables after removing a drawer', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
          mockSetProperty.mockClear()

          service.removeDrawer('left')

          expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
          expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '0px')
          expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '0px')
        })
      })
    })
  })

  describe('CSS Variables', () => {
    it('should update CSS variables when drawer opens', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '240px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '240px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '240px')
      })
    })

    it('should set drawer width to 0 when closed', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
        mockSetProperty.mockClear()

        service.setDrawerOpen('left', false)

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '0px')
      })
    })

    it('should update CSS variables when AppBar height changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.appBarHeight.setValue('64px')

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-appbar-height', '64px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-top', '64px')
      })
    })

    it('should update right drawer CSS variables', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.initDrawer('right', { open: true, width: '200px', variant: 'collapsible' })

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-width', '200px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-configured-width', '200px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-right', '200px')
      })
    })

    it('should set content margin to 0 for temporary drawer variant', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })

        // Drawer width is set to 200px (it's open)
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-width', '200px')
        // Configured width is always set
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-right-configured-width', '200px')
        // But content margin is 0 because temporary drawers overlay content
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-right', '0px')
      })
    })

    it('should always set content margin to drawer width for permanent drawer variant', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        // Test when closed
        service.initDrawer('left', { open: false, width: '240px', variant: 'permanent' })
        mockSetProperty.mockClear()

        // Even when closed, permanent drawers push content
        service.setDrawerOpen('left', false)

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '240px')
      })
    })

    it('should set content margin to 0 when collapsible drawer is closed', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
        mockSetProperty.mockClear()

        service.setDrawerOpen('left', false)

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
        // Configured width is still set
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '240px')
        // Content margin is 0 because drawer is closed
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '0px')
      })
    })

    it('should update CSS variables when topGap changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.setTopGap('16px')

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-top-gap', '16px')
        expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-padding-top', 'calc(48px + 16px)')
      })
    })

    it('should update CSS variables when sideGap changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        mockSetProperty.mockClear()

        service.setSideGap('24px')

        expect(mockSetProperty).toHaveBeenCalledWith('--layout-side-gap', '24px')
      })
    })
  })

  describe('LAYOUT_CSS_VARIABLES export', () => {
    it('should export all CSS variable names', () => {
      expect(LAYOUT_CSS_VARIABLES.appBarHeight).toBe('--layout-appbar-height')
      expect(LAYOUT_CSS_VARIABLES.topGap).toBe('--layout-top-gap')
      expect(LAYOUT_CSS_VARIABLES.sideGap).toBe('--layout-side-gap')
      expect(LAYOUT_CSS_VARIABLES.contentPaddingTop).toBe('--layout-content-padding-top')
      expect(LAYOUT_CSS_VARIABLES.drawerLeftWidth).toBe('--layout-drawer-left-width')
      expect(LAYOUT_CSS_VARIABLES.drawerRightWidth).toBe('--layout-drawer-right-width')
      expect(LAYOUT_CSS_VARIABLES.drawerLeftConfiguredWidth).toBe('--layout-drawer-left-configured-width')
      expect(LAYOUT_CSS_VARIABLES.drawerRightConfiguredWidth).toBe('--layout-drawer-right-configured-width')
      expect(LAYOUT_CSS_VARIABLES.contentMarginTop).toBe('--layout-content-margin-top')
      expect(LAYOUT_CSS_VARIABLES.contentMarginLeft).toBe('--layout-content-margin-left')
      expect(LAYOUT_CSS_VARIABLES.contentMarginRight).toBe('--layout-content-margin-right')
    })
  })

  describe('AppBar Visibility', () => {
    it('should initialize with visible AppBar', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        expect(service.appBarVisible.getValue()).toBe(true)
      })
    })

    it('should allow setting AppBar visibility', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        service.appBarVisible.setValue(false)

        expect(service.appBarVisible.getValue()).toBe(false)
      })
    })
  })

  describe('AppBar Height', () => {
    it('should initialize with default height', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        expect(service.appBarHeight.getValue()).toBe('48px')
      })
    })

    it('should allow setting AppBar height', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        service.appBarHeight.setValue('64px')

        expect(service.appBarHeight.getValue()).toBe('64px')
      })
    })
  })

  describe('Gap Management', () => {
    describe('topGap', () => {
      it('should initialize with default value', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          expect(service.topGap.getValue()).toBe('0px')
        })
      })

      it('should allow setting topGap via setTopGap', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setTopGap('16px')

          expect(service.topGap.getValue()).toBe('16px')
        })
      })

      it('should allow setting topGap via observable', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.topGap.setValue('32px')

          expect(service.topGap.getValue()).toBe('32px')
        })
      })
    })

    describe('sideGap', () => {
      it('should initialize with default value', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          expect(service.sideGap.getValue()).toBe('0px')
        })
      })

      it('should allow setting sideGap via setSideGap', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.setSideGap('24px')

          expect(service.sideGap.getValue()).toBe('24px')
        })
      })

      it('should allow setting sideGap via observable', () => {
        using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
          service.sideGap.setValue('48px')

          expect(service.sideGap.getValue()).toBe('48px')
        })
      })
    })
  })

  describe('Disposal', () => {
    it('should dispose all observables', () => {
      const service = createLayoutService(mockElement as unknown as HTMLElement)
      const drawerStateSpy = vi.spyOn(service.drawerState, Symbol.dispose)
      const appBarVisibleSpy = vi.spyOn(service.appBarVisible, Symbol.dispose)
      const appBarHeightSpy = vi.spyOn(service.appBarHeight, Symbol.dispose)
      const topGapSpy = vi.spyOn(service.topGap, Symbol.dispose)
      const sideGapSpy = vi.spyOn(service.sideGap, Symbol.dispose)

      service[Symbol.dispose]()

      expect(drawerStateSpy).toHaveBeenCalled()
      expect(appBarVisibleSpy).toHaveBeenCalled()
      expect(appBarHeightSpy).toHaveBeenCalled()
      expect(topGapSpy).toHaveBeenCalled()
      expect(sideGapSpy).toHaveBeenCalled()
    })
  })

  describe('Observable Subscriptions', () => {
    it('should notify subscribers when drawer state changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        const states: Array<{ left?: { open: boolean; width: string; variant: string } }> = []

        service.drawerState.subscribe((state) => {
          states.push(state)
        })

        service.initDrawer('left', { open: false, width: '240px', variant: 'collapsible' })
        service.setDrawerOpen('left', true)

        expect(states.length).toBe(2) // 2 changes (initDrawer + setDrawerOpen)
        expect(states[states.length - 1].left?.open).toBe(true)
        expect(states[states.length - 1].left?.variant).toBe('collapsible')
      })
    })

    it('should notify subscribers when topGap changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        const values: string[] = []

        service.topGap.subscribe((value) => {
          values.push(value)
        })

        service.setTopGap('16px')
        service.setTopGap('32px')

        expect(values).toEqual(['16px', '32px'])
      })
    })

    it('should notify subscribers when sideGap changes', () => {
      using(createLayoutService(mockElement as unknown as HTMLElement), (service) => {
        const values: string[] = []

        service.sideGap.subscribe((value) => {
          values.push(value)
        })

        service.setSideGap('24px')
        service.setSideGap('48px')

        expect(values).toEqual(['24px', '48px'])
      })
    })
  })

  describe('LAYOUT_CSS_VARIABLES', () => {
    it('should export all CSS variable names', () => {
      expect(LAYOUT_CSS_VARIABLES).toEqual({
        appBarHeight: '--layout-appbar-height',
        topGap: '--layout-top-gap',
        sideGap: '--layout-side-gap',
        contentPaddingTop: '--layout-content-padding-top',
        drawerLeftWidth: '--layout-drawer-left-width',
        drawerRightWidth: '--layout-drawer-right-width',
        drawerLeftConfiguredWidth: '--layout-drawer-left-configured-width',
        drawerRightConfiguredWidth: '--layout-drawer-right-configured-width',
        contentMarginTop: '--layout-content-margin-top',
        contentMarginLeft: '--layout-content-margin-left',
        contentMarginRight: '--layout-content-margin-right',
      })
    })
  })
})
