package hotkeys

import (
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// TestParseInputConf - parsing various input.conf line formats
// ---------------------------------------------------------------------------

func TestParseInputConf(t *testing.T) {
	tests := []struct {
		name              string
		input             string
		expectBindingKeys []string // Keys of actual bindings (non-comment/empty/section)
		expectComments    int
		expectEmpty       int
		expectSections    int
		expectTotal       int
	}{
		{
			name:              "normal binding",
			input:             "SPACE cycle pause",
			expectBindingKeys: []string{"SPACE"},
			expectTotal:       1,
		},
		{
			name:              "binding with inline comment",
			input:             "q quit # Quit playback",
			expectBindingKeys: []string{"q"},
			expectTotal:       1,
		},
		{
			name:           "comment line",
			input:          "# This is a comment",
			expectComments: 1,
			expectTotal:    1,
		},
		{
			name:        "empty line",
			input:       "",
			expectEmpty: 1,
			expectTotal: 1,
		},
		{
			name:           "section header",
			input:          "[default]",
			expectSections: 1,
			expectTotal:    1,
		},
		{
			name:              "multi-word command",
			input:             "Ctrl+Shift+RIGHT seek 30",
			expectBindingKeys: []string{"Ctrl+Shift+RIGHT"},
			expectTotal:       1,
		},
		{
			name: "mixed content",
			input: `# mpv input.conf preset
SPACE cycle pause

# Seeking
RIGHT seek 5
[default]
Ctrl+RIGHT seek 15`,
			expectBindingKeys: []string{"SPACE", "RIGHT", "Ctrl+RIGHT"},
			expectComments:    2,
			expectEmpty:       1,
			expectSections:    1,
			expectTotal:       7,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config, err := ParseInputConf(tt.input)
			if err != nil {
				t.Fatalf("ParseInputConf() unexpected error: %v", err)
			}

			if len(config.Bindings) != tt.expectTotal {
				t.Errorf("total bindings = %d, expected %d", len(config.Bindings), tt.expectTotal)
			}

			// Count types
			var actualBindings []string
			var comments, empties, sections int
			for _, b := range config.Bindings {
				switch {
				case b.IsComment:
					comments++
				case b.IsEmpty:
					empties++
				case b.IsSection:
					sections++
				default:
					actualBindings = append(actualBindings, b.Key)
				}
			}

			if comments != tt.expectComments {
				t.Errorf("comment lines = %d, expected %d", comments, tt.expectComments)
			}
			if empties != tt.expectEmpty {
				t.Errorf("empty lines = %d, expected %d", empties, tt.expectEmpty)
			}
			if sections != tt.expectSections {
				t.Errorf("section lines = %d, expected %d", sections, tt.expectSections)
			}
			if len(actualBindings) != len(tt.expectBindingKeys) {
				t.Errorf("binding count = %d, expected %d", len(actualBindings), len(tt.expectBindingKeys))
			} else {
				for i, key := range tt.expectBindingKeys {
					if actualBindings[i] != key {
						t.Errorf("binding[%d].Key = %q, expected %q", i, actualBindings[i], key)
					}
				}
			}
		})
	}
}

// TestParseBindingLineCommentExtraction verifies inline comments are captured.
func TestParseBindingLineCommentExtraction(t *testing.T) {
	input := "q quit # Quit playback"
	config, err := ParseInputConf(input)
	if err != nil {
		t.Fatalf("ParseInputConf() unexpected error: %v", err)
	}
	if len(config.Bindings) != 1 {
		t.Fatalf("expected 1 binding, got %d", len(config.Bindings))
	}
	b := config.Bindings[0]
	if b.Comment != " Quit playback" {
		t.Errorf("Comment = %q, expected %q", b.Comment, " Quit playback")
	}
	if b.Key != "q" {
		t.Errorf("Key = %q, expected %q", b.Key, "q")
	}
	if b.Command != "quit" {
		t.Errorf("Command = %q, expected %q", b.Command, "quit")
	}
}

// ---------------------------------------------------------------------------
// TestSerializeInputConf - roundtrip fidelity
// ---------------------------------------------------------------------------

func TestSerializeInputConf(t *testing.T) {
	tests := []struct {
		name  string
		input string
	}{
		{"simple binding", "SPACE cycle pause"},
		{"binding with comment", "q quit # Quit playback"},
		{"comment line", "# This is a comment"},
		{"section header", "[default]"},
		{"empty line", ""},
		{"multi-field command", "Ctrl+Shift+RIGHT seek 30"},
		{"full file", `# mpv input.conf preset
SPACE cycle pause

# Seeking
RIGHT seek 5
LEFT seek -5
[default]
Ctrl+RIGHT seek 15`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config, err := ParseInputConf(tt.input)
			if err != nil {
				t.Fatalf("ParseInputConf() unexpected error: %v", err)
			}
			serialized := SerializeInputConf(config)

			// Normalize for comparison: split and rejoin to handle trailing newline differences
			inputLines := strings.Split(tt.input, "\n")
			outputLines := strings.Split(serialized, "\n")

			// SerializeInputConf produces len(bindings) lines joined by "\n"
			// which is the same number of lines as input split by "\n"
			if len(outputLines) != len(inputLines) {
				t.Errorf("roundtrip line count = %d, expected %d", len(outputLines), len(inputLines))
				return
			}

			for i, want := range inputLines {
				got := outputLines[i]
				// For binding lines with inline comments, the serializer outputs " #Comment"
				// while input may have " # Comment". We just check the key/command portion.
				// For structural lines (empty, comment, section) we expect exact match.
				b := config.Bindings[i]
				if b.IsComment || b.IsEmpty || b.IsSection {
					// Section headers preserve RawLine exactly
					if b.IsSection {
						if got != want {
							t.Errorf("line %d: got %q, expected %q", i, got, want)
						}
					}
					// Empty lines are empty in both
					if b.IsEmpty {
						if got != "" {
							t.Errorf("line %d: expected empty, got %q", i, got)
						}
					}
				} else {
					// Binding: check key and command match
					if b.Key == "" {
						continue
					}
					if !strings.HasPrefix(got, b.Key+" "+b.Command) {
						t.Errorf("line %d: got %q, expected to start with %q", i, got, b.Key+" "+b.Command)
					}
				}
			}
		})
	}
}

// TestSerializeInputConfRoundtripSimple verifies a simple binding survives parse→serialize→parse.
func TestSerializeInputConfRoundtripSimple(t *testing.T) {
	original := "SPACE cycle pause"
	config1, _ := ParseInputConf(original)
	serialized := SerializeInputConf(config1)
	config2, _ := ParseInputConf(serialized)

	if len(config2.Bindings) != 1 {
		t.Fatalf("roundtrip: expected 1 binding, got %d", len(config2.Bindings))
	}
	if config2.Bindings[0].Key != "SPACE" {
		t.Errorf("roundtrip: Key = %q, expected %q", config2.Bindings[0].Key, "SPACE")
	}
	if config2.Bindings[0].Command != "cycle pause" {
		t.Errorf("roundtrip: Command = %q, expected %q", config2.Bindings[0].Command, "cycle pause")
	}
}

// ---------------------------------------------------------------------------
// TestFindBinding
// ---------------------------------------------------------------------------

func TestFindBinding(t *testing.T) {
	input := `# comment
SPACE cycle pause
RIGHT seek 5
Ctrl+RIGHT seek 15
`
	config, err := ParseInputConf(input)
	if err != nil {
		t.Fatalf("ParseInputConf() unexpected error: %v", err)
	}

	tests := []struct {
		name      string
		key       string
		expectCmd string
		expectNil bool
	}{
		{"found SPACE", "SPACE", "cycle pause", false},
		{"found RIGHT", "RIGHT", "seek 5", false},
		{"found Ctrl+RIGHT", "Ctrl+RIGHT", "seek 15", false},
		{"not found LEFT", "LEFT", "", true},
		{"not found - empty key", "", "", true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := FindBinding(config, tt.key)
			if tt.expectNil {
				if b != nil {
					t.Errorf("FindBinding(%q) expected nil, got %+v", tt.key, b)
				}
			} else {
				if b == nil {
					t.Fatalf("FindBinding(%q) expected non-nil", tt.key)
				}
				if b.Command != tt.expectCmd {
					t.Errorf("FindBinding(%q).Command = %q, expected %q", tt.key, b.Command, tt.expectCmd)
				}
			}
		})
	}
}

// ---------------------------------------------------------------------------
// TestSetBinding
// ---------------------------------------------------------------------------

func TestSetBinding(t *testing.T) {
	t.Run("add new binding", func(t *testing.T) {
		input := "SPACE cycle pause\nRIGHT seek 5\n"
		config, _ := ParseInputConf(input)

		SetBinding(config, "LEFT", "seek -5", "")

		b := FindBinding(config, "LEFT")
		if b == nil {
			t.Fatal("SetBinding: expected LEFT binding to exist")
		}
		if b.Command != "seek -5" {
			t.Errorf("SetBinding: Command = %q, expected %q", b.Command, "seek -5")
		}
	})

	t.Run("update existing binding", func(t *testing.T) {
		input := "SPACE cycle pause\nRIGHT seek 5\n"
		config, _ := ParseInputConf(input)

		SetBinding(config, "RIGHT", "seek 10", "faster seek")

		b := FindBinding(config, "RIGHT")
		if b == nil {
			t.Fatal("SetBinding: expected RIGHT binding to exist")
		}
		if b.Command != "seek 10" {
			t.Errorf("SetBinding: Command = %q, expected %q", b.Command, "seek 10")
		}
		if b.Comment != "faster seek" {
			t.Errorf("SetBinding: Comment = %q, expected %q", b.Comment, "faster seek")
		}
	})

	t.Run("preserves other bindings", func(t *testing.T) {
		input := "SPACE cycle pause\nRIGHT seek 5\n"
		config, _ := ParseInputConf(input)

		SetBinding(config, "LEFT", "seek -5", "")

		space := FindBinding(config, "SPACE")
		if space == nil || space.Command != "cycle pause" {
			t.Error("SetBinding: adding new binding should not affect existing ones")
		}
	})
}

// ---------------------------------------------------------------------------
// TestRemoveBinding
// ---------------------------------------------------------------------------

func TestRemoveBinding(t *testing.T) {
	t.Run("remove existing binding", func(t *testing.T) {
		input := "SPACE cycle pause\nRIGHT seek 5\nLEFT seek -5\n"
		config, _ := ParseInputConf(input)

		removed := RemoveBinding(config, "RIGHT")
		if !removed {
			t.Error("RemoveBinding: expected true (found and removed)")
		}
		if FindBinding(config, "RIGHT") != nil {
			t.Error("RemoveBinding: RIGHT should no longer exist")
		}
		// Other bindings should still be present
		if FindBinding(config, "SPACE") == nil {
			t.Error("RemoveBinding: SPACE should still exist")
		}
		if FindBinding(config, "LEFT") == nil {
			t.Error("RemoveBinding: LEFT should still exist")
		}
	})

	t.Run("remove non-existent binding", func(t *testing.T) {
		input := "SPACE cycle pause\n"
		config, _ := ParseInputConf(input)

		removed := RemoveBinding(config, "NONEXISTENT")
		if removed {
			t.Error("RemoveBinding: expected false for non-existent key")
		}
	})

	t.Run("remove preserves surrounding bindings", func(t *testing.T) {
		input := "SPACE cycle pause\nRIGHT seek 5\nLEFT seek -5\n"
		config, _ := ParseInputConf(input)
		originalLen := len(config.Bindings)

		RemoveBinding(config, "RIGHT")
		if len(config.Bindings) != originalLen-1 {
			t.Errorf("RemoveBinding: len = %d, expected %d", len(config.Bindings), originalLen-1)
		}
	})
}

// ---------------------------------------------------------------------------
// TestGetPresetList
// ---------------------------------------------------------------------------

func TestGetPresetList(t *testing.T) {
	presets := GetPresetList()

	if len(presets) != 3 {
		t.Fatalf("GetPresetList: expected 3 presets, got %d", len(presets))
	}

	expectedIDs := []string{"default", "vlc", "mpc-hc"}
	expectedNames := []string{"mpv Default", "VLC", "MPC-HC"}

	for i, p := range presets {
		if p.ID != expectedIDs[i] {
			t.Errorf("presets[%d].ID = %q, expected %q", i, p.ID, expectedIDs[i])
		}
		if p.Name != expectedNames[i] {
			t.Errorf("presets[%d].Name = %q, expected %q", i, p.Name, expectedNames[i])
		}
		if p.Description == "" {
			t.Errorf("presets[%d].Description should not be empty", i)
		}
	}
}

// ---------------------------------------------------------------------------
// TestLoadPreset
// ---------------------------------------------------------------------------

func TestLoadPreset(t *testing.T) {
	tests := []struct {
		name        string
		presetName  string
		wantError   bool
		errorSubstr string
	}{
		{"default preset", "default", false, ""},
		{"vlc preset", "vlc", false, ""},
		{"mpc-hc preset", "mpc-hc", false, ""},
		{"unknown preset", "nonexistent", true, "unknown hotkey preset"},
		{"empty preset name", "", true, "unknown hotkey preset"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			content, err := LoadPreset(tt.presetName)
			if (err != nil) != tt.wantError {
				t.Errorf("LoadPreset(%q) error = %v, wantError %v", tt.presetName, err, tt.wantError)
				return
			}
			if tt.wantError && err != nil {
				if !strings.Contains(err.Error(), tt.errorSubstr) {
					t.Errorf("LoadPreset(%q) error = %v, expected to contain %q", tt.presetName, err.Error(), tt.errorSubstr)
				}
				return
			}
			if !tt.wantError {
				if len(content) == 0 {
					t.Errorf("LoadPreset(%q): expected non-empty content", tt.presetName)
				}
				// Quick sanity: should contain at least one "cycle" command
				if !strings.Contains(content, "cycle") {
					t.Errorf("LoadPreset(%q): expected content to contain 'cycle'", tt.presetName)
				}
			}
		})
	}
}

// TestLoadPresetNoDuplicateKeys verifies that none of the embedded presets
// contain duplicate key bindings (the bug that was fixed for mpc-hc).
func TestLoadPresetNoDuplicateKeys(t *testing.T) {
	for _, preset := range GetPresetList() {
		t.Run(preset.ID, func(t *testing.T) {
			content, err := LoadPreset(preset.ID)
			if err != nil {
				t.Fatalf("LoadPreset(%q) error: %v", preset.ID, err)
			}

			config, err := ParseInputConf(content)
			if err != nil {
				t.Fatalf("ParseInputConf(%q) error: %v", preset.ID, err)
			}

			seen := make(map[string]bool)
			for _, b := range config.Bindings {
				if b.IsComment || b.IsEmpty || b.IsSection || b.Key == "" {
					continue
				}
				if seen[b.Key] {
					t.Errorf("preset %q has duplicate key %q", preset.ID, b.Key)
				}
				seen[b.Key] = true
			}
		})
	}
}
