package jira

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

	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

func TestFetchWorkItemCallsIssueEndpointAndMapsFields(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/rest/api/3/issue/PROJ-1" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		if r.Header.Get("Authorization") == "" {
			t.Fatalf("missing auth header: %#v", r.Header)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"key": "PROJ-1",
			"fields": map[string]any{
				"summary":     "Fix login bug",
				"description": json.RawMessage(`{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"This is the description"}]}]}`),
				"status":      map[string]string{"name": "In Progress"},
				"issuetype":   map[string]string{"name": "Task"},
				"labels":      []string{"frontend", "critical"},
			},
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "user@example.com", APIToken: "tok123"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "example", Project: "PROJ", ID: "PROJ-1"}
	item, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem returned error: %v", err)
	}
	if item.Title != "Fix login bug" {
		t.Errorf("title: got %q", item.Title)
	}
	if item.DescriptionText != "This is the description" {
		t.Errorf("description_text: got %q", item.DescriptionText)
	}
	if item.State != "In Progress" {
		t.Errorf("state: got %q", item.State)
	}
	if len(item.Labels) != 2 || item.Labels[0] != "frontend" || item.Labels[1] != "critical" {
		t.Errorf("labels: got %#v", item.Labels)
	}
	if item.Ref.ID != "PROJ-1" {
		t.Errorf("ref.ID: got %q", item.Ref.ID)
	}
}

func TestFetchWorkItemHandlesNilStatus(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"key": "PROJ-2",
			"fields": map[string]any{
				"summary":     "Untitled issue",
				"description": nil,
			},
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-2"}
	item, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem returned error: %v", err)
	}
	if item.State != "" {
		t.Errorf("nil status should produce empty state: got %q", item.State)
	}
}

func TestFetchWorkItemHandlesPlainTextDescription(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"key": "PROJ-3",
			"fields": map[string]any{
				"summary":     "Plain desc issue",
				"description": json.RawMessage(`"This description is plain text"`),
			},
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-3"}
	item, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem returned error: %v", err)
	}
	if item.DescriptionText != "This description is plain text" {
		t.Errorf("description_text: got %q", item.DescriptionText)
	}
}

func TestAppendCommentPostsADFBody(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/rest/api/3/issue/PROJ-1/comment" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body JiraCommentInput
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if !strings.Contains(string(body.Body), `"type":"paragraph"`) {
			t.Fatalf("comment body not in ADF: %s", string(body.Body))
		}
		w.WriteHeader(http.StatusCreated)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.AppendComment(context.Background(), workitem.CommentInput{
		Ref:            ref,
		Body:           "This is a comment body",
		Format:         workitem.CommentFormatText,
		ExternalSource: "nomadcode",
		ExternalID:     "task-1",
		Visibility:     "INTERNAL",
	})
	if err != nil {
		t.Fatalf("AppendComment returned error: %v", err)
	}
}

func TestAppendCommentRequiresIssueKey(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil)
	err := client.AppendComment(context.Background(), workitem.CommentInput{
		Ref:  workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: ""},
		Body: "hello",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty issue key: expected ErrInvalidInput, got %v", err)
	}
}

func TestProjectStatusPostsTransition(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/rest/api/3/issue/PROJ-1/transitions" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var transitionReq JiraTransitionRequest
		if err := json.NewDecoder(r.Body).Decode(&transitionReq); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if transitionReq.Transition.ID != "5" {
			t.Fatalf("transition ID: got %q", transitionReq.Transition.ID)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:            ref,
		CanonicalState: "in-progress",
		ProviderState:  "5",
	})
	if err != nil {
		t.Fatalf("ProjectStatus returned error: %v", err)
	}
}

func TestProjectStatusRequiresProviderState(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:           ref,
		ProviderState: "",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty provider state: expected ErrInvalidInput, got %v", err)
	}
}

func TestProjectStatusRequiresIssueKey(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil)
	err := client.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:           workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: ""},
		ProviderState: "5",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty issue key: expected ErrInvalidInput, got %v", err)
	}
}

func TestClientReturnsResponseBodyOnError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "forbidden: bad credentials", http.StatusForbidden)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	_, err := client.FetchWorkItem(context.Background(), ref)
	if err == nil || !strings.Contains(err.Error(), "status 403") || !strings.Contains(err.Error(), "bad credentials") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestClientImplementsWorkItemFacets(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil)

	if client.ProviderID() != workitem.ProviderID("jira") {
		t.Errorf("provider ID: got %q", client.ProviderID())
	}

	caps := client.Capabilities()
	if !caps.Supports(workitem.CapabilityLookup) {
		t.Error("expected lookup capability")
	}
	if !caps.Supports(workitem.CapabilityComment) {
		t.Error("expected comment capability")
	}
	if !caps.Supports(workitem.CapabilityStatusProjection) {
		t.Error("expected status projection capability")
	}
	if caps.Supports(workitem.CapabilityCreate) {
		t.Error("create should be unsupported")
	}
	if caps.Supports(workitem.CapabilityLabelProjection) {
		t.Error("label projection should be unsupported")
	}
}

func TestClientRequiresConfig(t *testing.T) {
	tests := []struct {
		name string
		cfg  Config
	}{
		{"empty", Config{}},
		{"no baseURL", Config{Email: "u@e.com", APIToken: "t1"}},
		{"no email", Config{BaseURL: "https://example.com", APIToken: "t1"}},
		{"no token", Config{BaseURL: "https://example.com", Email: "u@e.com"}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client := NewClient(tt.cfg, nil)
			ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
			_, err := client.FetchWorkItem(context.Background(), ref)
			if err == nil || !strings.Contains(err.Error(), ErrMissingConfig.Error()) {
				t.Fatalf("expected ErrMissingConfig, got %v", err)
			}
		})
	}
}

func TestExtractDescriptionTextFromADF(t *testing.T) {
	adf := json.RawMessage(`{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}`)
	got := extractDescriptionText(adf)
	if got != "Hello world" {
		t.Errorf("ADF text: got %q", got)
	}
}

func TestConvertCommentToADF(t *testing.T) {
	adf := convertCommentToADF("Hello\nWorld")
	var doc map[string]any
	if err := json.Unmarshal([]byte(adf), &doc); err != nil {
		t.Fatalf("convertCommentToADF produces invalid JSON: %v", err)
	}
	content, ok := doc["content"].([]any)
	if !ok || len(content) != 1 {
		t.Fatalf("expected one content node")
	}
	para, ok := content[0].(map[string]any)
	if !ok {
		t.Fatalf("content[0] not a map")
	}
	if para["type"] != "paragraph" {
		t.Errorf("expected paragraph type, got %s", para["type"])
	}
}

func TestConvertCommentToADFFormats(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var comment JiraCommentInput
		if err := json.NewDecoder(r.Body).Decode(&comment); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		var parsed map[string]any
		if err := json.Unmarshal(comment.Body, &parsed); err != nil {
			t.Fatalf("comment body is not valid JSON: %v", err)
		}
		content, _ := parsed["content"].([]any)
		if len(content) == 0 {
			t.Fatal("no content in ADF")
		}
		t.Logf("parsed ADF: %#v", content)
		w.WriteHeader(http.StatusCreated)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.AppendComment(context.Background(), workitem.CommentInput{
		Ref:            ref,
		Body:           "Test comment with special chars: \"quoted\" and &",
		Format:         workitem.CommentFormatText,
		ExternalSource: "nomadcode",
		ExternalID:     "task-2",
		Visibility:     "INTERNAL",
	})
	if err != nil {
		t.Fatalf("AppendComment returned error: %v", err)
	}
}

func TestFetchWorkItemInvalidInput(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: ""}
	_, err := client.FetchWorkItem(context.Background(), ref)
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty ref.ID: expected ErrInvalidInput, got %v", err)
	}
}

func TestNewClientWithHTTPClient(t *testing.T) {
	customClient := &http.Client{}
	client := NewClientWithHTTPClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, customClient, nil)
	if client.http != customClient {
		t.Fatal("expected custom http client")
	}
	nilClient := NewClientWithHTTPClient(Config{BaseURL: "https://example.com", Email: "u@e.com", APIToken: "t1"}, nil, nil)
	if nilClient.http != http.DefaultClient {
		t.Fatal("expected http.DefaultClient when nil passed")
	}
}

func TestFetchWorkItemNormalizesTrailingSlashInBaseURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/rest/api/3/issue/PROJ-1" {
			t.Fatalf("path: got %s (should not have double slash)", r.URL.Path)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"key":    "PROJ-1",
			"fields": map[string]any{"summary": "ok", "description": nil},
		})
	}))
	defer server.Close()

	serverWithSlash := server.URL + "/"
	client := NewClient(Config{BaseURL: serverWithSlash, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	_, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem with trailing slash config: unexpected error: %v", err)
	}
}

func TestFetchWorkItemNormalizesWhitespaceInBaseURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/rest/api/3/issue/PROJ-1" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"key":    "PROJ-1",
			"fields": map[string]any{"summary": "ok", "description": nil},
		})
	}))
	defer server.Close()

	spaced := "  " + server.URL + "  "
	client := NewClient(Config{BaseURL: spaced, Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	_, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem with whitespace config: unexpected error: %v", err)
	}
}

func TestAppendCommentNormalizesBaseURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/rest/api/3/issue/PROJ-1/comment" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		w.WriteHeader(http.StatusCreated)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL + "/", Email: "  u@e.com  ", APIToken: "  t1  "}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.AppendComment(context.Background(), workitem.CommentInput{
		Ref:            ref,
		Body:           "trimmed auth test",
		Format:         workitem.CommentFormatText,
		ExternalSource: "nomadcode",
		ExternalID:     "task-t",
		Visibility:     "internal",
	})
	if err != nil {
		t.Fatalf("AppendComment with trimmed config: unexpected error: %v", err)
	}
}

func TestProjectStatusNormalizesBaseURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/rest/api/3/issue/PROJ-1/transitions" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL + "/", Email: "u@e.com", APIToken: "t1"}, nil)
	ref := workitem.Ref{Provider: "jira", Tenant: "t", Project: "P", ID: "PROJ-1"}
	err := client.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:            ref,
		ProviderState:  "5",
		CanonicalState: "done",
	})
	if err != nil {
		t.Fatalf("ProjectStatus with trailing slash config: unexpected error: %v", err)
	}
}
