package issue

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"

	"github.com/gitlink-org/gitlink-cli/internal/client"
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

// testTags is a static name→id mapping used by unit tests.
var testTags = map[string]int{
	"缺陷": 315526,
	"功能": 315527,
	"文档": 315533,
	"任务": 315530,
	"测试": 315534,
}

// ---- helpers ----

func findShortcut(t *testing.T, name string) *common.Shortcut {
	t.Helper()
	for _, s := range Shortcuts() {
		if s.Name == name {
			return s
		}
	}
	t.Fatalf("shortcut %q not found", name)
	return nil
}

// mockTagsHandler returns a handler that responds to the issue_tags API.
func mockTagsHandler(t *testing.T) http.HandlerFunc {
	t.Helper()
	return func(w http.ResponseWriter, r *http.Request) {
		tags := make([]map[string]interface{}, 0, len(testTags))
		for name, id := range testTags {
			tags = append(tags, map[string]interface{}{
				"id":   float64(id),
				"name": name,
			})
		}
		writeJSONResp(t, w, map[string]interface{}{"issue_tags": tags})
	}
}

func runBatchCreateShortcut(t *testing.T, server *httptest.Server, args map[string]string) error {
	t.Helper()
	s := findShortcut(t, "batch-create")
	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner",
		Repo:   "repo",
		Format: "json",
		Args:   args,
	}
	return s.Run(ctx)
}

func writeJSONResp(t *testing.T, w http.ResponseWriter, v interface{}) {
	t.Helper()
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(v)
}

func decodeReqBody(t *testing.T, r *http.Request) map[string]interface{} {
	t.Helper()
	var payload map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	return payload
}

// ---- runBatchCreate tests ----

func TestBatchCreate_DryRun(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
			mockTagsHandler(t)(w, r)
			return
		}
		t.Fatalf("no API calls expected in dry-run mode, got %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{
		"titles":  "标题1,标题2",
		"dry-run": "true",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestBatchCreate_FromTitles(t *testing.T) {
	var createdBodies []map[string]interface{}
	callCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
			mockTagsHandler(t)(w, r)
		case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
			callCount++
			body := decodeReqBody(t, r)
			createdBodies = append(createdBodies, body)
			writeJSONResp(t, w, map[string]interface{}{
				"project_issues_index": float64(100 + callCount),
			})
		default:
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{
		"titles": "Bug修复,功能开发",
		"state":  "new",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if callCount != 2 {
		t.Fatalf("expected 2 API calls, got %d", callCount)
	}
	if createdBodies[0]["subject"] != "Bug修复" {
		t.Fatalf("first title: got %q, want %q", createdBodies[0]["subject"], "Bug修复")
	}
	if createdBodies[1]["subject"] != "功能开发" {
		t.Fatalf("second title: got %q, want %q", createdBodies[1]["subject"], "功能开发")
	}
	// Verify required fields — values come through JSON as float64
	for i, body := range createdBodies {
		if body["done_ratio"] != float64(0) {
			t.Fatalf("body[%d]: done_ratio = %v (type %T), want 0", i, body["done_ratio"], body["done_ratio"])
		}
	}
}

func TestBatchCreate_NoTitles(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
			mockTagsHandler(t)(w, r)
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{
		"dry-run": "false",
	})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestBatchCreate_FromCSV(t *testing.T) {
	csvPath := writeTempCSV(t, "title,priority,label,status\nCSV标题1,high,缺陷,new\nCSV标题2,normal,功能,new\n")

	var created []map[string]interface{}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
			mockTagsHandler(t)(w, r)
		case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
			created = append(created, decodeReqBody(t, r))
			writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(1)})
		default:
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{
		"from": csvPath,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(created) != 2 {
		t.Fatalf("expected 2 creates, got %d", len(created))
	}
	if created[0]["subject"] != "CSV标题1" {
		t.Fatalf("first subject: got %q", created[0]["subject"])
	}
	if created[1]["subject"] != "CSV标题2" {
		t.Fatalf("second subject: got %q", created[1]["subject"])
	}
}

func TestBatchCreate_CSVMissingTitleColumn(t *testing.T) {
	csvPath := writeTempCSV(t, "name,description\nval1,desc1\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
			mockTagsHandler(t)(w, r)
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{"from": csvPath})
	if err == nil {
		t.Fatal("expected error for missing title column, got nil")
	}
}

func TestBatchCreate_CSVOnlyHeader(t *testing.T) {
	csvPath := writeTempCSV(t, "title,description\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
			mockTagsHandler(t)(w, r)
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{"from": csvPath})
	if err == nil {
		t.Fatal("expected error for header-only CSV, got nil")
	}
}

func TestBatchCreate_PartialFailure(t *testing.T) {
	callCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
			mockTagsHandler(t)(w, r)
		case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
			callCount++
			if callCount == 2 {
				w.WriteHeader(http.StatusUnprocessableEntity)
				return
			}
			writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(callCount)})
		default:
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
	}))
	defer server.Close()

	err := runBatchCreateShortcut(t, server, map[string]string{
		"titles": "ok1,fail1,ok2",
	})
	if err == nil {
		t.Fatal("expected error from partial failure, got nil")
	}
	if !strings.Contains(err.Error(), "failed to create") {
		t.Fatalf("error should mention failed count, got: %v", err)
	}
}

// ---- buildCreateBody tests (direct call, values retain Go types) ----

func intVal(v interface{}) int {
	switch n := v.(type) {
	case int:
		return n
	case float64:
		return int(n)
	}
	return -999
}

func TestBuildCreateBody_Basic(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{Title: "Test issue", Status: "new"}
	body := buildCreateBody(ctx, input, "", testTags)
	if body["subject"] != "Test issue" {
		t.Fatalf("subject: got %v", body["subject"])
	}
	if intVal(body["done_ratio"]) != 0 {
		t.Fatalf("done_ratio: got %v (%T), want 0", body["done_ratio"], body["done_ratio"])
	}
	if intVal(body["status_id"]) != 1 {
		t.Fatalf("status_id: got %v (%T), want 1", body["status_id"], body["status_id"])
	}
	if intVal(body["priority_id"]) != 2 {
		t.Fatalf("priority_id: got %v (%T), want 2", body["priority_id"], body["priority_id"])
	}
}

func TestBuildCreateBody_BugTemplate(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{
		Title:    "登录报错",
		Version:  "v2.0",
		Severity: "严重",
		Steps:    "1. 打开页面\n2. 点击登录",
		Expected: "正常登录",
		Actual:   "报错 500",
	}
	body := buildCreateBody(ctx, input, "bug", testTags)
	if body["subject"] != "登录报错" {
		t.Fatalf("subject: got %v", body["subject"])
	}
	desc, _ := body["description"].(string)
	if !strings.Contains(desc, "## Bug 描述") {
		t.Fatal("bug description missing header")
	}
	if !strings.Contains(desc, "v2.0") {
		t.Fatal("bug description missing version")
	}
	if !strings.Contains(desc, "严重") {
		t.Fatal("bug description missing severity")
	}
	if rawTags, ok := body["issue_tag_ids"]; !ok {
		t.Fatal("bug template missing issue_tag_ids")
	} else {
		ids := rawTags.([]interface{})
		if intVal(ids[0]) != testTags["缺陷"] {
			t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"])
		}
	}
}

func TestBuildCreateBody_FeatureTemplate(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{
		Title:      "用户搜索",
		UserStory:  "作为用户，我想搜索内容",
		Acceptance: "搜索结果正确显示",
	}
	body := buildCreateBody(ctx, input, "feature", testTags)
	desc, _ := body["description"].(string)
	if !strings.Contains(desc, "## 用户故事") {
		t.Fatal("feature description missing user story header")
	}
	if !strings.Contains(desc, "作为用户") {
		t.Fatal("feature description missing user story content")
	}
	if !strings.Contains(desc, "## 验收标准") {
		t.Fatal("feature description missing acceptance criteria")
	}
}

func TestBuildCreateBody_WithPriorityLabel(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{
		Title:    "紧急修复",
		Priority: "high",
		Label:    "缺陷",
	}
	body := buildCreateBody(ctx, input, "", testTags)
	if intVal(body["priority_id"]) != 3 {
		t.Fatalf("priority_id: got %v (type %T), want 3 (high)", body["priority_id"], body["priority_id"])
	}
	if rawTags, ok := body["issue_tag_ids"]; ok {
		ids := rawTags.([]interface{})
		if intVal(ids[0]) != testTags["缺陷"] {
			t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"])
		}
	} else {
		t.Fatal("missing issue_tag_ids")
	}
}

// ---- buildBugDescription tests ----

func TestBuildBugDescription_AllFields(t *testing.T) {
	input := createIssueInput{
		Title:    "登录报错",
		Version:  "v2.0",
		Severity: "严重",
		Steps:    "1. 打开",
		Expected: "正常",
		Actual:   "500错误",
	}
	result := buildBugDescription(input)
	if !strings.Contains(result, "## Bug 描述") {
		t.Fatal("missing Bug 描述")
	}
	if !strings.Contains(result, "登录报错") {
		t.Fatal("missing title")
	}
	if !strings.Contains(result, "## 版本") {
		t.Fatal("missing 版本")
	}
	if !strings.Contains(result, "## 严重程度") {
		t.Fatal("missing 严重程度")
	}
	if !strings.Contains(result, "## 复现步骤") {
		t.Fatal("missing 复现步骤")
	}
	if !strings.Contains(result, "## 期望结果") {
		t.Fatal("missing 期望结果")
	}
	if !strings.Contains(result, "## 实际结果") {
		t.Fatal("missing 实际结果")
	}
}

func TestBuildBugDescription_PartialFields(t *testing.T) {
	input := createIssueInput{Title: "小问题"}
	result := buildBugDescription(input)
	if !strings.Contains(result, "## Bug 描述") {
		t.Fatal("missing header")
	}
	if strings.Contains(result, "## 版本") {
		t.Fatal("should not have version section")
	}
	if strings.Contains(result, "## 严重程度") {
		t.Fatal("should not have severity section")
	}
}

// ---- buildFeatureDescription tests ----

func TestBuildFeatureDescription_AllFields(t *testing.T) {
	input := createIssueInput{
		Title:      "搜索功能",
		UserStory:  "作为用户想搜索",
		Body:       "详细描述",
		Acceptance: "搜索结果正确",
		Priority:   "high",
	}
	result := buildFeatureDescription(input)
	if !strings.Contains(result, "## 用户故事") {
		t.Fatal("missing user story")
	}
	if !strings.Contains(result, "作为用户想搜索") {
		t.Fatal("missing user story content")
	}
	if !strings.Contains(result, "## 描述") {
		t.Fatal("missing description")
	}
	if !strings.Contains(result, "## 验收标准") {
		t.Fatal("missing acceptance criteria")
	}
	if !strings.Contains(result, "## 优先级") {
		t.Fatal("missing priority")
	}
}

func TestBuildFeatureDescription_FallbackToTitleAsUserStory(t *testing.T) {
	input := createIssueInput{Title: "搜索功能"}
	result := buildFeatureDescription(input)
	if !strings.Contains(result, "搜索功能") {
		t.Fatal("should fall back to title as user story")
	}
}

// ---- readCreateInputsFromCSV tests ----

func TestReadCreateInputsFromCSV_Normal(t *testing.T) {
	path := writeTempCSV(t, "title,priority,label,status,version,severity,steps,expected,actual\n标题1,high,缺陷,new,v1,严重,,,\n标题2,normal,功能,new,,,,,\n")

	inputs, err := readCreateInputsFromCSV(path, "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(inputs) != 2 {
		t.Fatalf("got %d inputs, want 2", len(inputs))
	}
	if inputs[0].Title != "标题1" {
		t.Fatalf("first title: got %q", inputs[0].Title)
	}
	if inputs[0].Severity != "严重" {
		t.Fatalf("severity: got %q", inputs[0].Severity)
	}
	if inputs[1].Label != "功能" {
		t.Fatalf("label: got %q", inputs[1].Label)
	}
}

func TestReadCreateInputsFromCSV_MissingTitleColumn(t *testing.T) {
	path := writeTempCSV(t, "name,description\nval1,desc1\n")
	_, err := readCreateInputsFromCSV(path, "")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestReadCreateInputsFromCSV_OnlyHeader(t *testing.T) {
	path := writeTempCSV(t, "title,priority\n")
	_, err := readCreateInputsFromCSV(path, "")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestReadCreateInputsFromCSV_SkipsEmptyTitle(t *testing.T) {
	path := writeTempCSV(t, "title,priority\n标题1,high\n,normal\n标题2,low\n")
	inputs, err := readCreateInputsFromCSV(path, "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(inputs) != 2 {
		t.Fatalf("got %d inputs, want 2 (empty row skipped)", len(inputs))
	}
}

// ---- parseTitles tests ----

func TestParseTitles_CommaSeparated(t *testing.T) {
	ctx := &common.RuntimeContext{
		Owner: "o", Repo: "r",
		Args: map[string]string{"priority": "normal"},
	}
	inputs := parseTitles("标题1, 标题2, , 标题3", ctx)
	if len(inputs) != 3 {
		t.Fatalf("got %d inputs, want 3", len(inputs))
	}
	if inputs[0].Title != "标题1" {
		t.Fatalf("got %q", inputs[0].Title)
	}
	if inputs[2].Title != "标题3" {
		t.Fatalf("got %q", inputs[2].Title)
	}
	if inputs[0].Priority != "normal" {
		t.Fatalf("priority not propagated: got %q", inputs[0].Priority)
	}
}

// ---- normalizeHeader tests ----

func TestNormalizeHeader(t *testing.T) {
	cases := []struct{ in, want string }{
		{"Title", "title"},
		{"  PRIORITY ", "priority"},
		{"user_story", "user_story"},
		{"User Story", "user story"},
	}
	for _, c := range cases {
		got := normalizeHeader(c.in)
		if got != c.want {
			t.Fatalf("normalizeHeader(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

// ---- getCol tests ----

func TestGetCol(t *testing.T) {
	col := map[string]int{"title": 0, "priority": 1}
	record := []string{"测试标题", "high"}
	if got := getCol(record, col, "title"); got != "测试标题" {
		t.Fatalf("got %q", got)
	}
	if got := getCol(record, col, "missing"); got != "" {
		t.Fatalf("got %q, want empty", got)
	}
	if got := getCol(record, col, "priority"); got != "high" {
		t.Fatalf("got %q", got)
	}
}

// ---- truncate tests ----

func TestTruncate(t *testing.T) {
	if got := truncate("short", 40); got != "short" {
		t.Fatalf("got %q", got)
	}
	long := "这是一个很长的标题用来测试截断功能一二三四五六七八九十"
	got := truncate(long, 10)
	if len([]rune(got)) > 13 {
		t.Fatalf("truncated too long: %q (%d runes)", got, len([]rune(got)))
	}
	if !strings.HasSuffix(got, "...") {
		t.Fatal("truncated string should end with ...")
	}
}

// ---- priority/label/status parse helpers ----

func TestParsePriorityStrings(t *testing.T) {
	cases := []struct {
		in   string
		want int
	}{
		{"low", 1}, {"normal", 2}, {"high", 3}, {"urgent", 4},
		{"LOW", 1}, {"High", 3},
	}
	for _, c := range cases {
		got, err := parsePriority(c.in)
		if err != nil {
			t.Fatalf("parsePriority(%q): %v", c.in, err)
		}
		if got != c.want {
			t.Fatalf("parsePriority(%q) = %d, want %d", c.in, got, c.want)
		}
	}
}

func TestParsePriorityNumeric(t *testing.T) {
	got, err := parsePriority("5")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 5 {
		t.Fatalf("got %d, want 5", got)
	}
}

func TestParsePriorityInvalid(t *testing.T) {
	if _, err := parsePriority("invalid"); err == nil {
		t.Fatal("expected error")
	}
}

func TestParseLabelValid(t *testing.T) {
	id, err := parseLabel("缺陷", testTags)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if id != testTags["缺陷"] {
		t.Fatalf("got %d, want %d", id, testTags["缺陷"])
	}
}

func TestParseLabelInvalid(t *testing.T) {
	if _, err := parseLabel("不存在的标签", testTags); err == nil {
		t.Fatal("expected error")
	}
}

func TestParseLabelNumeric(t *testing.T) {
	id, err := parseLabel("999", testTags)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if id != 999 {
		t.Fatalf("got %d, want 999", id)
	}
}

func TestLabelNamesReturnsAll(t *testing.T) {
	names := labelNames(testTags)
	if !strings.Contains(names, "缺陷") {
		t.Fatal("missing 缺陷 in label names")
	}
	if !strings.Contains(names, "功能") {
		t.Fatal("missing 功能 in label names")
	}
}

// ---- buildCreateBody status tests ----

func TestBuildCreateBody_DefaultStatus(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{Title: "t", Status: ""}
	body := buildCreateBody(ctx, input, "", testTags)
	if intVal(body["status_id"]) != 1 {
		t.Fatalf("default status_id: got %v (type %T), want 1", body["status_id"], body["status_id"])
	}
}

func TestBuildCreateBody_ClosedStatus(t *testing.T) {
	ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
	input := createIssueInput{Title: "t", Status: "closed"}
	body := buildCreateBody(ctx, input, "", testTags)
	if intVal(body["status_id"]) != 5 {
		t.Fatalf("closed status_id: got %v (type %T), want 5", body["status_id"], body["status_id"])
	}
}

// ---- regression ----

func TestCollectIssueNumbers_FromBatchCreatePerspective(t *testing.T) {
	got, err := collectIssueNumbers("1,2,3", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	want := []string{"1", "2", "3"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("got %v, want %v", got, want)
	}
}
