package wlroutput

import (
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)

func TestStateChanged_BothNil(t *testing.T) {
	assert.True(t, stateChanged(nil, nil))
}

func TestStateChanged_OneNil(t *testing.T) {
	s := &State{Serial: 1}
	assert.True(t, stateChanged(s, nil))
	assert.True(t, stateChanged(nil, s))
}

func TestStateChanged_SerialDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{}}
	b := &State{Serial: 2, Outputs: []Output{}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputCountDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
	b := &State{Serial: 1, Outputs: []Output{}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputNameDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "HDMI-A-1", Enabled: true}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputEnabledDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: false}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputPositionDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 0, Y: 0}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 1920, Y: 0}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputTransformDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 0}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 1}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputScaleDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 1.0}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 2.0}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputAdaptiveSyncDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 0}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 1}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_CurrentModeNilVsNonNil(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: &OutputMode{Width: 1920}}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_CurrentModeDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{
		Name:        "eDP-1",
		CurrentMode: &OutputMode{Width: 1920, Height: 1080, Refresh: 60000},
	}}}
	b := &State{Serial: 1, Outputs: []Output{{
		Name:        "eDP-1",
		CurrentMode: &OutputMode{Width: 2560, Height: 1440, Refresh: 60000},
	}}}
	assert.True(t, stateChanged(a, b))

	b.Outputs[0].CurrentMode.Width = 1920
	b.Outputs[0].CurrentMode.Height = 1080
	b.Outputs[0].CurrentMode.Refresh = 144000
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_ModesLengthDiffers(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}}}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}, {Width: 1280}}}}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_Equal(t *testing.T) {
	mode := OutputMode{Width: 1920, Height: 1080, Refresh: 60000, Preferred: true}
	a := &State{
		Serial: 5,
		Outputs: []Output{{
			Name:           "eDP-1",
			Description:    "Built-in display",
			Make:           "BOE",
			Model:          "0x0ABC",
			SerialNumber:   "12345",
			PhysicalWidth:  309,
			PhysicalHeight: 174,
			Enabled:        true,
			X:              0,
			Y:              0,
			Transform:      0,
			Scale:          1.0,
			CurrentMode:    &mode,
			Modes:          []OutputMode{mode},
			AdaptiveSync:   0,
		}},
	}
	b := &State{
		Serial: 5,
		Outputs: []Output{{
			Name:           "eDP-1",
			Description:    "Built-in display",
			Make:           "BOE",
			Model:          "0x0ABC",
			SerialNumber:   "12345",
			PhysicalWidth:  309,
			PhysicalHeight: 174,
			Enabled:        true,
			X:              0,
			Y:              0,
			Transform:      0,
			Scale:          1.0,
			CurrentMode:    &mode,
			Modes:          []OutputMode{mode},
			AdaptiveSync:   0,
		}},
	}
	assert.False(t, stateChanged(a, b))
}

func TestManager_ConcurrentGetState(t *testing.T) {
	m := &Manager{
		state: &State{
			Serial:  1,
			Outputs: []Output{{Name: "eDP-1", Enabled: true}},
		},
	}

	var wg sync.WaitGroup
	const goroutines = 50
	const iterations = 100

	for i := 0; i < goroutines/2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				s := m.GetState()
				_ = s.Serial
				_ = s.Outputs
			}
		}()
	}

	for i := 0; i < goroutines/2; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				m.stateMutex.Lock()
				m.state = &State{
					Serial:  uint32(j),
					Outputs: []Output{{Name: "eDP-1", Scale: float64(j % 3)}},
				}
				m.stateMutex.Unlock()
			}
		}(i)
	}

	wg.Wait()
}

func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
	m := &Manager{
		stopChan: make(chan struct{}),
		dirty:    make(chan struct{}, 1),
	}

	var wg sync.WaitGroup
	const goroutines = 20

	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			subID := string(rune('a' + id))
			ch := m.Subscribe(subID)
			assert.NotNil(t, ch)
			time.Sleep(time.Millisecond)
			m.Unsubscribe(subID)
		}(i)
	}

	wg.Wait()
}

func TestManager_SyncmapHeadsConcurrentAccess(t *testing.T) {
	m := &Manager{}

	var wg sync.WaitGroup
	const goroutines = 30
	const iterations = 50

	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			key := uint32(id)

			for j := 0; j < iterations; j++ {
				state := &headState{
					id:      key,
					name:    "test-head",
					enabled: j%2 == 0,
					scale:   float64(j % 3),
					modeIDs: []uint32{uint32(j)},
				}
				m.heads.Store(key, state)

				if loaded, ok := m.heads.Load(key); ok {
					assert.Equal(t, key, loaded.id)
				}

				m.heads.Range(func(k uint32, v *headState) bool {
					_ = v.name
					_ = v.enabled
					return true
				})
			}

			m.heads.Delete(key)
		}(i)
	}

	wg.Wait()
}

func TestManager_SyncmapModesConcurrentAccess(t *testing.T) {
	m := &Manager{}

	var wg sync.WaitGroup
	const goroutines = 30
	const iterations = 50

	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			key := uint32(id)

			for j := 0; j < iterations; j++ {
				state := &modeState{
					id:        key,
					width:     int32(1920 + j),
					height:    int32(1080 + j),
					refresh:   60000,
					preferred: j == 0,
				}
				m.modes.Store(key, state)

				if loaded, ok := m.modes.Load(key); ok {
					assert.Equal(t, key, loaded.id)
				}

				m.modes.Range(func(k uint32, v *modeState) bool {
					_ = v.width
					_ = v.height
					return true
				})
			}

			m.modes.Delete(key)
		}(i)
	}

	wg.Wait()
}

func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
	m := &Manager{
		dirty: make(chan struct{}, 1),
	}

	for i := 0; i < 10; i++ {
		m.notifySubscribers()
	}

	assert.Len(t, m.dirty, 1)
}

func TestManager_PostQueueFull(t *testing.T) {
	m := &Manager{
		cmdq:     make(chan cmd, 2),
		stopChan: make(chan struct{}),
	}

	m.post(func() {})
	m.post(func() {})
	m.post(func() {})
	m.post(func() {})

	assert.Len(t, m.cmdq, 2)
}

func TestManager_GetStateNilState(t *testing.T) {
	m := &Manager{}

	s := m.GetState()
	assert.NotNil(t, s.Outputs)
	assert.Equal(t, uint32(0), s.Serial)
}

func TestManager_FatalErrorChannel(t *testing.T) {
	m := &Manager{
		fatalError: make(chan error, 1),
	}

	ch := m.FatalError()
	assert.NotNil(t, ch)

	m.fatalError <- assert.AnError
	err := <-ch
	assert.Error(t, err)
}

func TestOutputMode_Fields(t *testing.T) {
	mode := OutputMode{
		Width:     1920,
		Height:    1080,
		Refresh:   60000,
		Preferred: true,
		ID:        42,
	}

	assert.Equal(t, int32(1920), mode.Width)
	assert.Equal(t, int32(1080), mode.Height)
	assert.Equal(t, int32(60000), mode.Refresh)
	assert.True(t, mode.Preferred)
	assert.Equal(t, uint32(42), mode.ID)
}

func TestOutput_Fields(t *testing.T) {
	out := Output{
		Name:           "eDP-1",
		Description:    "Built-in display",
		Make:           "BOE",
		Model:          "0x0ABC",
		SerialNumber:   "12345",
		PhysicalWidth:  309,
		PhysicalHeight: 174,
		Enabled:        true,
		X:              0,
		Y:              0,
		Transform:      0,
		Scale:          1.5,
		AdaptiveSync:   1,
		ID:             1,
	}

	assert.Equal(t, "eDP-1", out.Name)
	assert.Equal(t, "Built-in display", out.Description)
	assert.True(t, out.Enabled)
	assert.Equal(t, float64(1.5), out.Scale)
	assert.Equal(t, uint32(1), out.AdaptiveSync)
}

func TestHeadState_ModeIDsSlice(t *testing.T) {
	head := &headState{
		id:      1,
		modeIDs: make([]uint32, 0),
	}

	head.modeIDs = append(head.modeIDs, 1, 2, 3)
	assert.Len(t, head.modeIDs, 3)
	assert.Equal(t, uint32(1), head.modeIDs[0])
}

func TestStateChanged_BothCurrentModeNil(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
	assert.False(t, stateChanged(a, b))
}

func TestStateChanged_IndexOutOfBounds(t *testing.T) {
	a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
	b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}, {Name: "HDMI-A-1"}}}
	assert.True(t, stateChanged(a, b))
}

func TestNewManager_GetRegistryError(t *testing.T) {
	mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)

	mockDisplay.EXPECT().Context().Return(nil)
	mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))

	_, err := NewManager(mockDisplay)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to get registry")
}
