package utils

import (
	"context"
	"testing"

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

func TestParseIssueID(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		prefix   string
		expected string
	}{
		{
			name:     "already has prefix",
			input:    "bd-a3f8e9",
			prefix:   "bd-",
			expected: "bd-a3f8e9",
		},
		{
			name:     "missing prefix",
			input:    "a3f8e9",
			prefix:   "bd-",
			expected: "bd-a3f8e9",
		},
		{
			name:     "hierarchical with prefix",
			input:    "bd-a3f8e9.1.2",
			prefix:   "bd-",
			expected: "bd-a3f8e9.1.2",
		},
		{
			name:     "hierarchical without prefix",
			input:    "a3f8e9.1.2",
			prefix:   "bd-",
			expected: "bd-a3f8e9.1.2",
		},
		{
			name:     "custom prefix with ID",
			input:    "ticket-123",
			prefix:   "ticket-",
			expected: "ticket-123",
		},
		{
			name:     "custom prefix without ID",
			input:    "123",
			prefix:   "ticket-",
			expected: "ticket-123",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ParseIssueID(tt.input, tt.prefix)
			if result != tt.expected {
				t.Errorf("ParseIssueID(%q, %q) = %q; want %q", tt.input, tt.prefix, result, tt.expected)
			}
		})
	}
}

func TestResolvePartialID(t *testing.T) {
	ctx := context.Background()
	store := memory.New("")
	
	// Create test issues with sequential IDs (current implementation)
	// When hash IDs (bd-165) are implemented, these can be hash-based
	issue1 := &types.Issue{
		ID:        "bd-1",
		Title:     "Test Issue 1",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	issue2 := &types.Issue{
		ID:        "bd-2",
		Title:     "Test Issue 2",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	issue3 := &types.Issue{
		ID:        "bd-10",
		Title:     "Test Issue 3",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	// Test hierarchical IDs - parent and child
	parentIssue := &types.Issue{
		ID:        "offlinebrew-3d0",
		Title:     "Parent Epic",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeEpic,
	}
	childIssue := &types.Issue{
		ID:        "offlinebrew-3d0.1",
		Title:     "Child Task",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	
	if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
		t.Fatal(err)
	}
	if err := store.CreateIssue(ctx, issue2, "test"); err != nil {
		t.Fatal(err)
	}
	if err := store.CreateIssue(ctx, issue3, "test"); err != nil {
		t.Fatal(err)
	}
	if err := store.CreateIssue(ctx, parentIssue, "test"); err != nil {
		t.Fatal(err)
	}
	if err := store.CreateIssue(ctx, childIssue, "test"); err != nil {
		t.Fatal(err)
	}
	
	// Set config for prefix
	if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name        string
		input       string
		expected    string
		shouldError bool
		errorMsg    string
	}{
		{
			name:     "exact match with prefix",
			input:    "bd-1",
			expected: "bd-1",
		},
		{
			name:     "exact match without prefix",
			input:    "1",
			expected: "bd-1",
		},
		{
			name:     "exact match with prefix (two digits)",
			input:    "bd-10",
			expected: "bd-10",
		},
		{
			name:     "exact match without prefix (two digits)",
			input:    "10",
			expected: "bd-10",
		},
		{
			name:        "nonexistent issue",
			input:       "bd-999",
			shouldError: true,
			errorMsg:    "no issue found",
		},
		{
			name:     "partial match - unique substring",
			input:    "bd-1",
			expected: "bd-1",
		},
		{
			name:        "ambiguous partial match",
			input:       "bd-1",
			expected:    "bd-1",  // Will match exactly, not ambiguously
		},
		{
			name:     "exact match parent ID with hierarchical child - gh-316",
			input:    "offlinebrew-3d0",
			expected: "offlinebrew-3d0",  // Should match exactly, not be ambiguous with offlinebrew-3d0.1
		},
		{
			name:     "exact match parent without prefix - gh-316",
			input:    "3d0",
			expected: "offlinebrew-3d0",  // Should still prefer exact hash match
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := ResolvePartialID(ctx, store, tt.input)
			
			if tt.shouldError {
				if err == nil {
					t.Errorf("ResolvePartialID(%q) expected error containing %q, got nil", tt.input, tt.errorMsg)
				} else if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) {
					t.Errorf("ResolvePartialID(%q) error = %q; want error containing %q", tt.input, err.Error(), tt.errorMsg)
				}
			} else {
				if err != nil {
					t.Errorf("ResolvePartialID(%q) unexpected error: %v", tt.input, err)
				}
				if result != tt.expected {
					t.Errorf("ResolvePartialID(%q) = %q; want %q", tt.input, result, tt.expected)
				}
			}
		})
	}
}

func TestResolvePartialIDs(t *testing.T) {
	ctx := context.Background()
	store := memory.New("")
	
	// Create test issues
	issue1 := &types.Issue{
		ID:        "bd-1",
		Title:     "Test Issue 1",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	issue2 := &types.Issue{
		ID:        "bd-2",
		Title:     "Test Issue 2",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	
	if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
		t.Fatal(err)
	}
	if err := store.CreateIssue(ctx, issue2, "test"); err != nil {
		t.Fatal(err)
	}
	
	if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name        string
		inputs      []string
		expected    []string
		shouldError bool
	}{
		{
			name:     "resolve multiple IDs without prefix",
			inputs:   []string{"1", "2"},
			expected: []string{"bd-1", "bd-2"},
		},
		{
			name:     "resolve mixed full and partial IDs",
			inputs:   []string{"bd-1", "2"},
			expected: []string{"bd-1", "bd-2"},
		},
		{
			name:        "error on nonexistent ID",
			inputs:      []string{"1", "999"},
			shouldError: true,
		},
		{
			name:     "empty input list",
			inputs:   []string{},
			expected: []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := ResolvePartialIDs(ctx, store, tt.inputs)
			
			if tt.shouldError {
				if err == nil {
					t.Errorf("ResolvePartialIDs(%v) expected error, got nil", tt.inputs)
				}
			} else {
				if err != nil {
					t.Errorf("ResolvePartialIDs(%v) unexpected error: %v", tt.inputs, err)
				}
				if len(result) != len(tt.expected) {
					t.Errorf("ResolvePartialIDs(%v) returned %d results; want %d", tt.inputs, len(result), len(tt.expected))
				}
				for i := range result {
					if result[i] != tt.expected[i] {
						t.Errorf("ResolvePartialIDs(%v)[%d] = %q; want %q", tt.inputs, i, result[i], tt.expected[i])
					}
				}
			}
		})
	}
}

func TestResolvePartialID_NoConfig(t *testing.T) {
	ctx := context.Background()
	store := memory.New("")
	
	// Create test issue without setting config (test default prefix)
	issue1 := &types.Issue{
		ID:        "bd-1",
		Title:     "Test Issue",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}
	
	if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
		t.Fatal(err)
	}
	
	// Don't set config - should use default "bd" prefix
	result, err := ResolvePartialID(ctx, store, "1")
	if err != nil {
		t.Fatalf("ResolvePartialID failed with default config: %v", err)
	}
	
	if result != "bd-1" {
		t.Errorf("ResolvePartialID(\"1\") with default config = %q; want \"bd-1\"", result)
	}
}

func TestExtractIssuePrefix(t *testing.T) {
	tests := []struct {
		name     string
		issueID  string
		expected string
	}{
		{
			name:     "standard format",
			issueID:  "bd-a3f8e9",
			expected: "bd",
		},
		{
			name:     "custom prefix",
			issueID:  "ticket-123",
			expected: "ticket",
		},
		{
			name:     "hierarchical ID",
			issueID:  "bd-a3f8e9.1.2",
			expected: "bd",
		},
		{
			name:     "no hyphen",
			issueID:  "invalid",
			expected: "",
		},
		{
			name:     "empty string",
			issueID:  "",
			expected: "",
		},
		{
			name:     "only prefix",
			issueID:  "bd-",
			expected: "bd",
		},
		{
			name:     "multi-part prefix with numeric suffix",
			issueID:  "alpha-beta-1",
			expected: "alpha-beta", // Last hyphen before numeric suffix
		},
		{
			name:     "multi-part non-numeric suffix (word-like)",
			issueID:  "vc-baseline-test",
			expected: "vc", // Word-like suffix (4+ chars, no digit) uses first hyphen (bd-fasa fix)
		},
		{
			name:     "beads-vscode style prefix",
			issueID:  "beads-vscode-1",
			expected: "beads-vscode", // Last hyphen before numeric suffix
		},
		{
			name:     "web-app style prefix",
			issueID:  "web-app-123",
			expected: "web-app", // Should extract "web-app", not "web-"
		},
		{
			name:     "three-part prefix with hash",
			issueID:  "my-cool-app-a3f8e9",
			expected: "my-cool-app", // Hash suffix should use last hyphen logic
		},
		{
			name:     "four-part prefix with 4-char hash",
			issueID:  "super-long-project-name-1a2b",
			expected: "super-long-project-name", // 4-char hash
		},
		{
			name:     "prefix with 5-char hash",
			issueID:  "my-app-1a2b3",
			expected: "my-app", // 5-char hash
		},
		{
			name:     "prefix with 6-char hash",
			issueID:  "web-app-a1b2c3",
			expected: "web-app", // 6-char hash
		},
		{
			name:     "uppercase hash",
			issueID:  "my-app-A3F8E9",
			expected: "my-app", // Uppercase hash should work
		},
		{
			name:     "mixed case hash",
			issueID:  "proj-AbCd12",
			expected: "proj", // Mixed case hash should work
		},
		{
			name:     "3-char hash with hyphenated prefix",
			issueID:  "document-intelligence-0sa",
			expected: "document-intelligence", // 3-char hash (base36) should use last hyphen
		},
		{
			name:     "3-char hash with multi-part prefix",
			issueID:  "my-cool-app-1x7",
			expected: "my-cool-app", // 3-char base36 hash
		},
		{
			name:     "3-char all-letters suffix (now treated as hash, GH #446)",
			issueID:  "test-proj-abc",
			expected: "test-proj", // 3-char all-letter now accepted as hash (GH #446)
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ExtractIssuePrefix(tt.issueID)
			if result != tt.expected {
				t.Errorf("ExtractIssuePrefix(%q) = %q; want %q", tt.issueID, result, tt.expected)
			}
		})
	}
}

func TestExtractIssueNumber(t *testing.T) {
	tests := []struct {
		name     string
		issueID  string
		expected int
	}{
		{
			name:     "simple number",
			issueID:  "bd-123",
			expected: 123,
		},
		{
			name:     "hash ID (no number)",
			issueID:  "bd-a3f8e9",
			expected: 0,
		},
		{
			name:     "hierarchical with number",
			issueID:  "bd-42.1.2",
			expected: 42,
		},
		{
			name:     "no hyphen",
			issueID:  "invalid",
			expected: 0,
		},
		{
			name:     "empty string",
			issueID:  "",
			expected: 0,
		},
		{
			name:     "zero",
			issueID:  "bd-0",
			expected: 0,
		},
		{
			name:     "large number",
			issueID:  "bd-999999",
			expected: 999999,
		},
		{
			name:     "number with text after",
			issueID:  "bd-123abc",
			expected: 123,
		},
		{
			name:     "hyphenated prefix with number",
			issueID:  "alpha-beta-7",
			expected: 7,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ExtractIssueNumber(tt.issueID)
			if result != tt.expected {
				t.Errorf("ExtractIssueNumber(%q) = %d; want %d", tt.issueID, result, tt.expected)
			}
		})
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && 
		(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || 
		findSubstring(s, substr)))
}

func findSubstring(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
