package dwl

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{TagCount: 9}
	assert.True(t, stateChanged(s, nil))
	assert.True(t, stateChanged(nil, s))
}

func TestStateChanged_TagCountDiffers(t *testing.T) {
	a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}}
	b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_LayoutLengthDiffers(t *testing.T) {
	a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)}
	b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_ActiveOutputDiffers(t *testing.T) {
	a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
	b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputCountDiffers(t *testing.T) {
	a := &State{
		TagCount: 9,
		Outputs:  map[string]*OutputState{"eDP-1": {}},
		Layouts:  []string{},
	}
	b := &State{
		TagCount: 9,
		Outputs:  map[string]*OutputState{},
		Layouts:  []string{},
	}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_OutputFieldsDiffer(t *testing.T) {
	a := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Active: 1, Layout: 0, Title: "Firefox"},
		},
	}
	b := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Active: 0, Layout: 0, Title: "Firefox"},
		},
	}
	assert.True(t, stateChanged(a, b))

	b.Outputs["eDP-1"].Active = 1
	b.Outputs["eDP-1"].Layout = 1
	assert.True(t, stateChanged(a, b))

	b.Outputs["eDP-1"].Layout = 0
	b.Outputs["eDP-1"].Title = "Code"
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_TagsDiffer(t *testing.T) {
	a := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}},
		},
	}
	b := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}},
		},
	}
	assert.True(t, stateChanged(a, b))

	b.Outputs["eDP-1"].Tags[0].State = 1
	b.Outputs["eDP-1"].Tags[0].Clients = 3
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_Equal(t *testing.T) {
	a := &State{
		TagCount:     9,
		ActiveOutput: "eDP-1",
		Layouts:      []string{"tile", "monocle"},
		Outputs: map[string]*OutputState{
			"eDP-1": {
				Name:         "eDP-1",
				Active:       1,
				Layout:       0,
				LayoutSymbol: "[]=",
				Title:        "Firefox",
				AppID:        "firefox",
				KbLayout:     "us",
				Keymode:      "",
				Tags:         []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
			},
		},
	}
	b := &State{
		TagCount:     9,
		ActiveOutput: "eDP-1",
		Layouts:      []string{"tile", "monocle"},
		Outputs: map[string]*OutputState{
			"eDP-1": {
				Name:         "eDP-1",
				Active:       1,
				Layout:       0,
				LayoutSymbol: "[]=",
				Title:        "Firefox",
				AppID:        "firefox",
				KbLayout:     "us",
				Keymode:      "",
				Tags:         []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
			},
		},
	}
	assert.False(t, stateChanged(a, b))
}

func TestManager_ConcurrentGetState(t *testing.T) {
	m := &Manager{
		state: &State{
			TagCount: 9,
			Layouts:  []string{"tile"},
			Outputs:  map[string]*OutputState{"eDP-1": {Name: "eDP-1"}},
		},
	}

	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.TagCount
				_ = 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{
					TagCount: uint32(j % 10),
					Layouts:  []string{"tile", "monocle"},
					Outputs:  map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}},
				}
				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_SyncmapOutputsConcurrentAccess(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 := &outputState{
					id:     key,
					name:   "test-output",
					active: uint32(j % 2),
					tags:   []TagState{{Tag: uint32(j), State: 1}},
				}
				m.outputs.Store(key, state)

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

				m.outputs.Range(func(k uint32, v *outputState) bool {
					_ = v.name
					_ = v.active
					return true
				})
			}

			m.outputs.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.NotNil(t, s.Layouts)
	assert.Equal(t, uint32(0), s.TagCount)
}

func TestTagState_Fields(t *testing.T) {
	tag := TagState{
		Tag:     1,
		State:   2,
		Clients: 3,
		Focused: 1,
	}

	assert.Equal(t, uint32(1), tag.Tag)
	assert.Equal(t, uint32(2), tag.State)
	assert.Equal(t, uint32(3), tag.Clients)
	assert.Equal(t, uint32(1), tag.Focused)
}

func TestOutputState_Fields(t *testing.T) {
	out := OutputState{
		Name:         "eDP-1",
		Active:       1,
		Tags:         []TagState{{Tag: 1}},
		Layout:       0,
		LayoutSymbol: "[]=",
		Title:        "Firefox",
		AppID:        "firefox",
		KbLayout:     "us",
		Keymode:      "",
	}

	assert.Equal(t, "eDP-1", out.Name)
	assert.Equal(t, uint32(1), out.Active)
	assert.Len(t, out.Tags, 1)
	assert.Equal(t, "[]=", out.LayoutSymbol)
}

func TestStateChanged_NewOutputAppears(t *testing.T) {
	a := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Name: "eDP-1"},
		},
	}
	b := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1":    {Name: "eDP-1"},
			"HDMI-A-1": {Name: "HDMI-A-1"},
		},
	}
	assert.True(t, stateChanged(a, b))
}

func TestStateChanged_TagsLengthDiffers(t *testing.T) {
	a := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Tags: []TagState{{Tag: 1}}},
		},
	}
	b := &State{
		TagCount: 9,
		Layouts:  []string{},
		Outputs: map[string]*OutputState{
			"eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}},
		},
	}
	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")
}
