package linear

import (
	"testing"
	"time"

	"github.com/steveyegge/beads/internal/types"
)

func TestGenerateIssueIDs(t *testing.T) {
	// Create test issues without IDs
	issues := []*types.Issue{
		{
			Title:       "First issue",
			Description: "Description 1",
			CreatedAt:   time.Now(),
		},
		{
			Title:       "Second issue",
			Description: "Description 2",
			CreatedAt:   time.Now().Add(-time.Hour),
		},
		{
			Title:       "Third issue",
			Description: "Description 3",
			CreatedAt:   time.Now().Add(-2 * time.Hour),
		},
	}

	// Generate IDs
	err := GenerateIssueIDs(issues, "test", "linear-import", IDGenerationOptions{})
	if err != nil {
		t.Fatalf("GenerateIssueIDs failed: %v", err)
	}

	// Verify all issues have IDs
	for i, issue := range issues {
		if issue.ID == "" {
			t.Errorf("Issue %d has empty ID", i)
		}
		// Verify prefix
		if !hasPrefix(issue.ID, "test-") {
			t.Errorf("Issue %d ID '%s' doesn't have prefix 'test-'", i, issue.ID)
		}
	}

	// Verify all IDs are unique
	seen := make(map[string]bool)
	for i, issue := range issues {
		if seen[issue.ID] {
			t.Errorf("Duplicate ID found: %s (issue %d)", issue.ID, i)
		}
		seen[issue.ID] = true
	}
}

func TestGenerateIssueIDsPreservesExisting(t *testing.T) {
	existingID := "test-existing"
	issues := []*types.Issue{
		{
			ID:          existingID,
			Title:       "Existing issue",
			Description: "Has an ID already",
			CreatedAt:   time.Now(),
		},
		{
			Title:       "New issue",
			Description: "Needs an ID",
			CreatedAt:   time.Now(),
		},
	}

	err := GenerateIssueIDs(issues, "test", "linear-import", IDGenerationOptions{})
	if err != nil {
		t.Fatalf("GenerateIssueIDs failed: %v", err)
	}

	// First issue should keep its original ID
	if issues[0].ID != existingID {
		t.Errorf("Existing ID was changed: got %s, want %s", issues[0].ID, existingID)
	}

	// Second issue should have a new ID
	if issues[1].ID == "" {
		t.Error("Second issue has empty ID")
	}
	if issues[1].ID == existingID {
		t.Error("Second issue has same ID as first (collision)")
	}
}

func TestGenerateIssueIDsNoDuplicates(t *testing.T) {
	// Create issues with identical content - should still get unique IDs
	now := time.Now()
	issues := []*types.Issue{
		{
			Title:       "Same title",
			Description: "Same description",
			CreatedAt:   now,
		},
		{
			Title:       "Same title",
			Description: "Same description",
			CreatedAt:   now,
		},
	}

	err := GenerateIssueIDs(issues, "bd", "linear-import", IDGenerationOptions{})
	if err != nil {
		t.Fatalf("GenerateIssueIDs failed: %v", err)
	}

	// Both should have IDs
	if issues[0].ID == "" || issues[1].ID == "" {
		t.Error("One or both issues have empty IDs")
	}

	// IDs should be different (nonce handles collision)
	if issues[0].ID == issues[1].ID {
		t.Errorf("Both issues have same ID: %s", issues[0].ID)
	}
}

func TestNormalizeIssueForLinearHashCanonicalizesExternalRef(t *testing.T) {
	slugged := "https://linear.app/crown-dev/issue/BEA-93/updated-title-for-beads"
	canonical := "https://linear.app/crown-dev/issue/BEA-93"
	issue := &types.Issue{
		Title:       "Title",
		Description: "Description",
		ExternalRef: &slugged,
	}

	normalized := NormalizeIssueForLinearHash(issue)
	if normalized.ExternalRef == nil {
		t.Fatal("expected external_ref to be present")
	}
	if *normalized.ExternalRef != canonical {
		t.Fatalf("expected canonical external_ref %q, got %q", canonical, *normalized.ExternalRef)
	}
}

func hasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

func TestDefaultMappingConfig(t *testing.T) {
	config := DefaultMappingConfig()

	// Check priority mappings
	if config.PriorityMap["0"] != 4 {
		t.Errorf("PriorityMap[0] = %d, want 4", config.PriorityMap["0"])
	}
	if config.PriorityMap["1"] != 0 {
		t.Errorf("PriorityMap[1] = %d, want 0", config.PriorityMap["1"])
	}

	// Check state mappings
	if config.StateMap["backlog"] != "open" {
		t.Errorf("StateMap[backlog] = %s, want open", config.StateMap["backlog"])
	}
	if config.StateMap["started"] != "in_progress" {
		t.Errorf("StateMap[started] = %s, want in_progress", config.StateMap["started"])
	}
	if config.StateMap["completed"] != "closed" {
		t.Errorf("StateMap[completed] = %s, want closed", config.StateMap["completed"])
	}

	// Check label type mappings
	if config.LabelTypeMap["bug"] != "bug" {
		t.Errorf("LabelTypeMap[bug] = %s, want bug", config.LabelTypeMap["bug"])
	}
	if config.LabelTypeMap["feature"] != "feature" {
		t.Errorf("LabelTypeMap[feature] = %s, want feature", config.LabelTypeMap["feature"])
	}

	// Check relation mappings
	if config.RelationMap["blocks"] != "blocks" {
		t.Errorf("RelationMap[blocks] = %s, want blocks", config.RelationMap["blocks"])
	}
}

func TestPriorityToBeads(t *testing.T) {
	config := DefaultMappingConfig()

	tests := []struct {
		linearPriority int
		want           int
	}{
		{0, 4}, // No priority -> Backlog
		{1, 0}, // Urgent -> Critical
		{2, 1}, // High -> High
		{3, 2}, // Medium -> Medium
		{4, 3}, // Low -> Low
		{5, 2}, // Unknown -> Medium (default)
	}

	for _, tt := range tests {
		got := PriorityToBeads(tt.linearPriority, config)
		if got != tt.want {
			t.Errorf("PriorityToBeads(%d) = %d, want %d", tt.linearPriority, got, tt.want)
		}
	}
}

func TestPriorityToLinear(t *testing.T) {
	config := DefaultMappingConfig()

	tests := []struct {
		beadsPriority int
		want          int
	}{
		{0, 1}, // Critical -> Urgent
		{1, 2}, // High -> High
		{2, 3}, // Medium -> Medium
		{3, 4}, // Low -> Low
		{4, 0}, // Backlog -> No priority
		{5, 3}, // Unknown -> Medium (default)
	}

	for _, tt := range tests {
		got := PriorityToLinear(tt.beadsPriority, config)
		if got != tt.want {
			t.Errorf("PriorityToLinear(%d) = %d, want %d", tt.beadsPriority, got, tt.want)
		}
	}
}

func TestStateToBeadsStatus(t *testing.T) {
	config := DefaultMappingConfig()

	tests := []struct {
		state *State
		want  types.Status
	}{
		{nil, types.StatusOpen},
		{&State{Type: "backlog", Name: "Backlog"}, types.StatusOpen},
		{&State{Type: "unstarted", Name: "Todo"}, types.StatusOpen},
		{&State{Type: "started", Name: "In Progress"}, types.StatusInProgress},
		{&State{Type: "completed", Name: "Done"}, types.StatusClosed},
		{&State{Type: "canceled", Name: "Cancelled"}, types.StatusClosed},
		{&State{Type: "unknown", Name: "Unknown"}, types.StatusOpen}, // Default
	}

	for _, tt := range tests {
		got := StateToBeadsStatus(tt.state, config)
		if got != tt.want {
			stateName := "nil"
			if tt.state != nil {
				stateName = tt.state.Type
			}
			t.Errorf("StateToBeadsStatus(%s) = %v, want %v", stateName, got, tt.want)
		}
	}
}

func TestParseBeadsStatus(t *testing.T) {
	tests := []struct {
		input string
		want  types.Status
	}{
		{"open", types.StatusOpen},
		{"OPEN", types.StatusOpen},
		{"in_progress", types.StatusInProgress},
		{"in-progress", types.StatusInProgress},
		{"inprogress", types.StatusInProgress},
		{"blocked", types.StatusBlocked},
		{"closed", types.StatusClosed},
		{"CLOSED", types.StatusClosed},
		{"unknown", types.StatusOpen}, // Default
	}

	for _, tt := range tests {
		got := ParseBeadsStatus(tt.input)
		if got != tt.want {
			t.Errorf("ParseBeadsStatus(%q) = %v, want %v", tt.input, got, tt.want)
		}
	}
}

func TestStatusToLinearStateType(t *testing.T) {
	tests := []struct {
		status types.Status
		want   string
	}{
		{types.StatusOpen, "unstarted"},
		{types.StatusInProgress, "started"},
		{types.StatusBlocked, "started"},
		{types.StatusClosed, "completed"},
		{types.Status("unknown"), "unstarted"}, // Unknown -> default
	}

	for _, tt := range tests {
		got := StatusToLinearStateType(tt.status)
		if got != tt.want {
			t.Errorf("StatusToLinearStateType(%v) = %q, want %q", tt.status, got, tt.want)
		}
	}
}

func TestLabelToIssueType(t *testing.T) {
	config := DefaultMappingConfig()

	tests := []struct {
		labels *Labels
		want   types.IssueType
	}{
		{nil, types.TypeTask},
		{&Labels{Nodes: []Label{}}, types.TypeTask},
		{&Labels{Nodes: []Label{{Name: "bug"}}}, types.TypeBug},
		{&Labels{Nodes: []Label{{Name: "Bug"}}}, types.TypeBug},
		{&Labels{Nodes: []Label{{Name: "feature"}}}, types.TypeFeature},
		{&Labels{Nodes: []Label{{Name: "epic"}}}, types.TypeEpic},
		{&Labels{Nodes: []Label{{Name: "chore"}}}, types.TypeChore},
		{&Labels{Nodes: []Label{{Name: "random"}, {Name: "bug"}}}, types.TypeBug},
		{&Labels{Nodes: []Label{{Name: "contains-bug-keyword"}}}, types.TypeBug},
	}

	for _, tt := range tests {
		got := LabelToIssueType(tt.labels, config)
		if got != tt.want {
			t.Errorf("LabelToIssueType(%v) = %v, want %v", tt.labels, got, tt.want)
		}
	}
}

func TestParseIssueType(t *testing.T) {
	tests := []struct {
		input string
		want  types.IssueType
	}{
		{"bug", types.TypeBug},
		{"BUG", types.TypeBug},
		{"feature", types.TypeFeature},
		{"task", types.TypeTask},
		{"epic", types.TypeEpic},
		{"chore", types.TypeChore},
		{"unknown", types.TypeTask}, // Default
	}

	for _, tt := range tests {
		got := ParseIssueType(tt.input)
		if got != tt.want {
			t.Errorf("ParseIssueType(%q) = %v, want %v", tt.input, got, tt.want)
		}
	}
}

func TestRelationToBeadsDep(t *testing.T) {
	config := DefaultMappingConfig()

	tests := []struct {
		relationType string
		want         string
	}{
		{"blocks", "blocks"},
		{"blockedBy", "blocks"},
		{"duplicate", "duplicates"},
		{"related", "related"},
		{"unknown", "related"}, // Default
	}

	for _, tt := range tests {
		got := RelationToBeadsDep(tt.relationType, config)
		if got != tt.want {
			t.Errorf("RelationToBeadsDep(%q) = %q, want %q", tt.relationType, got, tt.want)
		}
	}
}

func TestIssueToBeads(t *testing.T) {
	config := DefaultMappingConfig()

	linearIssue := &Issue{
		ID:          "uuid-123",
		Identifier:  "PROJ-123",
		Title:       "Test Issue",
		Description: "Test description",
		URL:         "https://linear.app/team/issue/PROJ-123/test-issue",
		Priority:    2, // High
		State:       &State{Type: "started", Name: "In Progress"},
		Assignee:    &User{Name: "John Doe", Email: "john@example.com"},
		Labels:      &Labels{Nodes: []Label{{Name: "bug"}}},
		CreatedAt:   "2024-01-15T10:00:00Z",
		UpdatedAt:   "2024-01-16T12:00:00Z",
	}

	result := IssueToBeads(linearIssue, config)
	issue := result.Issue.(*types.Issue)

	if issue.Title != "Test Issue" {
		t.Errorf("Title = %q, want %q", issue.Title, "Test Issue")
	}
	if issue.Description != "Test description" {
		t.Errorf("Description = %q, want %q", issue.Description, "Test description")
	}
	if issue.Priority != 1 { // High in beads
		t.Errorf("Priority = %d, want 1", issue.Priority)
	}
	if issue.Status != types.StatusInProgress {
		t.Errorf("Status = %v, want %v", issue.Status, types.StatusInProgress)
	}
	if issue.Assignee != "john@example.com" {
		t.Errorf("Assignee = %q, want %q", issue.Assignee, "john@example.com")
	}
	if issue.IssueType != types.TypeBug {
		t.Errorf("IssueType = %v, want %v", issue.IssueType, types.TypeBug)
	}
	if issue.ExternalRef == nil {
		t.Error("ExternalRef should not be nil")
	}
}

func TestIssueToBeadsWithParent(t *testing.T) {
	config := DefaultMappingConfig()

	linearIssue := &Issue{
		ID:          "uuid-456",
		Identifier:  "PROJ-456",
		Title:       "Child Issue",
		Description: "Child description",
		URL:         "https://linear.app/team/issue/PROJ-456",
		Priority:    3,
		State:       &State{Type: "unstarted", Name: "Todo"},
		Parent:      &Parent{ID: "uuid-123", Identifier: "PROJ-123"},
		CreatedAt:   "2024-01-15T10:00:00Z",
		UpdatedAt:   "2024-01-16T12:00:00Z",
	}

	result := IssueToBeads(linearIssue, config)

	if len(result.Dependencies) != 1 {
		t.Fatalf("Expected 1 dependency, got %d", len(result.Dependencies))
	}
	if result.Dependencies[0].Type != "parent-child" {
		t.Errorf("Dependency type = %q, want %q", result.Dependencies[0].Type, "parent-child")
	}
	if result.Dependencies[0].FromLinearID != "PROJ-456" {
		t.Errorf("FromLinearID = %q, want %q", result.Dependencies[0].FromLinearID, "PROJ-456")
	}
	if result.Dependencies[0].ToLinearID != "PROJ-123" {
		t.Errorf("ToLinearID = %q, want %q", result.Dependencies[0].ToLinearID, "PROJ-123")
	}
}

func TestBuildLinearToLocalUpdates(t *testing.T) {
	config := DefaultMappingConfig()

	linearIssue := &Issue{
		ID:          "uuid-123",
		Identifier:  "PROJ-123",
		Title:       "Updated Title",
		Description: "Updated description",
		Priority:    1, // Urgent
		State:       &State{Type: "completed", Name: "Done"},
		Assignee:    &User{Name: "Jane Doe", Email: "jane@example.com"},
		Labels:      &Labels{Nodes: []Label{{Name: "feature"}, {Name: "priority"}}},
		UpdatedAt:   "2024-01-20T15:00:00Z",
		CompletedAt: "2024-01-20T14:00:00Z",
	}

	updates := BuildLinearToLocalUpdates(linearIssue, config)

	if updates["title"] != "Updated Title" {
		t.Errorf("title = %v, want %q", updates["title"], "Updated Title")
	}
	if updates["description"] != "Updated description" {
		t.Errorf("description = %v, want %q", updates["description"], "Updated description")
	}
	if updates["priority"] != 0 { // Critical in beads
		t.Errorf("priority = %v, want 0", updates["priority"])
	}
	if updates["status"] != "closed" {
		t.Errorf("status = %v, want %q", updates["status"], "closed")
	}
	if updates["assignee"] != "jane@example.com" {
		t.Errorf("assignee = %v, want %q", updates["assignee"], "jane@example.com")
	}

	labels, ok := updates["labels"].([]string)
	if !ok || len(labels) != 2 {
		t.Errorf("labels = %v, want 2 labels", updates["labels"])
	}
}

func TestBuildLinearToLocalUpdatesNoAssignee(t *testing.T) {
	config := DefaultMappingConfig()

	linearIssue := &Issue{
		ID:          "uuid-123",
		Identifier:  "PROJ-123",
		Title:       "No Assignee",
		Description: "Test",
		Priority:    3,
		State:       &State{Type: "unstarted", Name: "Todo"},
		Assignee:    nil,
		UpdatedAt:   "2024-01-20T15:00:00Z",
	}

	updates := BuildLinearToLocalUpdates(linearIssue, config)

	if updates["assignee"] != "" {
		t.Errorf("assignee = %v, want empty string", updates["assignee"])
	}
}

// mockConfigLoader implements ConfigLoader for testing
type mockConfigLoader struct {
	config map[string]string
}

func (m *mockConfigLoader) GetAllConfig() (map[string]string, error) {
	return m.config, nil
}

func TestLoadMappingConfig(t *testing.T) {
	loader := &mockConfigLoader{
		config: map[string]string{
			"linear.priority_map.0":       "3",
			"linear.state_map.custom":     "in_progress",
			"linear.label_type_map.story": "feature",
			"linear.relation_map.parent":  "parent-child",
		},
	}

	config := LoadMappingConfig(loader)

	// Check custom priority mapping
	if config.PriorityMap["0"] != 3 {
		t.Errorf("PriorityMap[0] = %d, want 3", config.PriorityMap["0"])
	}

	// Check custom state mapping
	if config.StateMap["custom"] != "in_progress" {
		t.Errorf("StateMap[custom] = %s, want in_progress", config.StateMap["custom"])
	}

	// Check custom label type mapping
	if config.LabelTypeMap["story"] != "feature" {
		t.Errorf("LabelTypeMap[story] = %s, want feature", config.LabelTypeMap["story"])
	}

	// Check custom relation mapping
	if config.RelationMap["parent"] != "parent-child" {
		t.Errorf("RelationMap[parent] = %s, want parent-child", config.RelationMap["parent"])
	}

	// Check that defaults are preserved
	if config.StateMap["started"] != "in_progress" {
		t.Errorf("StateMap[started] = %s, want in_progress (default preserved)", config.StateMap["started"])
	}
}

func TestLoadMappingConfigNilLoader(t *testing.T) {
	config := LoadMappingConfig(nil)

	// Should return defaults
	if config.PriorityMap["1"] != 0 {
		t.Errorf("Expected default priority map with nil loader")
	}
}

func TestBuildLinearDescription(t *testing.T) {
	tests := []struct {
		name  string
		issue *types.Issue
		want  string
	}{
		{
			name:  "description only",
			issue: &types.Issue{Description: "Basic description"},
			want:  "Basic description",
		},
		{
			name: "with acceptance criteria",
			issue: &types.Issue{
				Description:        "Main description",
				AcceptanceCriteria: "- Must do X\n- Must do Y",
			},
			want: "Main description\n\n## Acceptance Criteria\n- Must do X\n- Must do Y",
		},
		{
			name: "with all fields",
			issue: &types.Issue{
				Description:        "Main description",
				AcceptanceCriteria: "AC here",
				Design:             "Design notes",
				Notes:              "Additional notes",
			},
			want: "Main description\n\n## Acceptance Criteria\nAC here\n\n## Design\nDesign notes\n\n## Notes\nAdditional notes",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := BuildLinearDescription(tt.issue)
			if got != tt.want {
				t.Errorf("BuildLinearDescription() = %q, want %q", got, tt.want)
			}
		})
	}
}
