package plane

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

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

func TestGetWorkItemUsesWorkItemsEndpointAndAPIKey(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 != "/api/v1/workspaces/general/projects/project-1/work-items/work-1" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		if r.Header.Get("X-Api-Key") != "test-token" {
			t.Fatalf("missing API key header: %#v", r.Header)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":                   "work-1",
			"name":                 "Fix issue",
			"description_stripped": "Do the thing",
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	item, err := client.GetWorkItem(context.Background(), WorkItemRef{
		WorkspaceSlug: "general",
		ProjectID:     "project-1",
		WorkItemID:    "work-1",
	})
	if err != nil {
		t.Fatalf("GetWorkItem returned error: %v", err)
	}
	if item.ID != "work-1" || item.Name != "Fix issue" || item.DescriptionStripped != "Do the thing" {
		t.Fatalf("unexpected item: %#v", item)
	}
}

func TestAddCommentPostsHTMLAndExternalFields(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 != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/comments/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		if r.Header.Get("Content-Type") != "application/json" {
			t.Fatalf("content type: got %s", r.Header.Get("Content-Type"))
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["comment_html"] != "<p>done</p>" || body["external_source"] != "nomadcode" || body["external_id"] != "task-1" {
			t.Fatalf("unexpected body: %#v", body)
		}
		w.WriteHeader(http.StatusCreated)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err := client.AddComment(context.Background(), AddCommentInput{
		Ref: WorkItemRef{
			WorkspaceSlug: "general",
			ProjectID:     "project-1",
			WorkItemID:    "work-1",
		},
		CommentHTML:    "<p>done</p>",
		ExternalSource: "nomadcode",
		ExternalID:     "task-1",
	})
	if err != nil {
		t.Fatalf("AddComment returned error: %v", err)
	}
}

func TestUpdateIssueStatusPatchesState(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["state"] != "state-1" {
			t.Fatalf("unexpected body: %#v", body)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err := client.UpdateIssueStatus(context.Background(), UpdateIssueStatusInput{
		Ref: WorkItemRef{
			WorkspaceSlug: "general",
			ProjectID:     "project-1",
			WorkItemID:    "work-1",
		},
		Status: "state-1",
	})
	if err != nil {
		t.Fatalf("UpdateIssueStatus returned error: %v", err)
	}
}

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

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	_, err := client.GetWorkItem(context.Background(), WorkItemRef{
		WorkspaceSlug: "general",
		ProjectID:     "project-1",
		WorkItemID:    "work-1",
	})
	if err == nil || !strings.Contains(err.Error(), "status 403") || !strings.Contains(err.Error(), "not allowed") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestClientImplementsWorkItemFacets(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Token: "test-token"}, nil)
	if client.ProviderID() != workitem.ProviderID("plane") {
		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("expected create capability")
	}
	if !caps.Supports(workitem.CapabilityBodyProjection) {
		t.Error("expected body projection capability")
	}
	if caps.Supports(workitem.CapabilityLabelProjection) {
		t.Error("label projection should be unsupported")
	}
}

func TestUpdateWorkItemPatchesOnlyNonEmptyFields(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["name"] != "New title" {
			t.Errorf("name: got %q", body["name"])
		}
		if body["description_html"] != "<p>new body</p>" {
			t.Errorf("description_html: got %q", body["description_html"])
		}
		if _, ok := body["state"]; ok {
			t.Errorf("state should not be present when blank: %#v", body)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err := client.UpdateWorkItem(context.Background(), UpdateWorkItemInput{
		Ref: WorkItemRef{
			WorkspaceSlug: "general",
			ProjectID:     "project-1",
			WorkItemID:    "work-1",
		},
		Title:           "New title",
		DescriptionHTML: "<p>new body</p>",
	})
	if err != nil {
		t.Fatalf("UpdateWorkItem returned error: %v", err)
	}
}

func TestUpdateWorkItemRejectsEmptyPatch(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Token: "test-token"}, nil)
	err := client.UpdateWorkItem(context.Background(), UpdateWorkItemInput{
		Ref: WorkItemRef{WorkspaceSlug: "general", ProjectID: "project-1", WorkItemID: "work-1"},
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty patch: expected ErrInvalidInput, got %v", err)
	}
}

func TestProjectBodyPatchesTitleAndBody(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["name"] != "[m-slug] Title" {
			t.Errorf("name: got %q", body["name"])
		}
		if body["description_html"] != "<p>milestone</p>" {
			t.Errorf("description_html: got %q", body["description_html"])
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err := client.ProjectBody(context.Background(), workitem.BodyProjection{
		Ref:             workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"},
		Title:           "[m-slug] Title",
		DescriptionHTML: "<p>milestone</p>",
	})
	if err != nil {
		t.Fatalf("ProjectBody returned error: %v", err)
	}
}

func TestProjectBodyRejectsEmptyInput(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Token: "test-token"}, nil)
	err := client.ProjectBody(context.Background(), workitem.BodyProjection{
		Ref: workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"},
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty body projection: expected ErrInvalidInput, got %v", err)
	}
}

func TestCreateIssuePostsWorkItem(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 != "/api/v1/workspaces/general/projects/project-1/work-items/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		if r.Header.Get("X-Api-Key") != "test-token" {
			t.Fatalf("missing API key header: %#v", r.Header)
		}
		if r.Header.Get("Content-Type") != "application/json" {
			t.Fatalf("content type: got %s", r.Header.Get("Content-Type"))
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["name"] != "New work item" {
			t.Errorf("name: got %q", body["name"])
		}
		if body["description_html"] != "<p>do it</p>" {
			t.Errorf("description_html: got %q", body["description_html"])
		}
		w.WriteHeader(http.StatusCreated)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":   "work-new",
			"name": "New work item",
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	item, err := client.CreateIssue(context.Background(), CreateIssueInput{
		WorkspaceSlug:   "general",
		ProjectID:       "project-1",
		Title:           "New work item",
		DescriptionHTML: "<p>do it</p>",
	})
	if err != nil {
		t.Fatalf("CreateIssue returned error: %v", err)
	}
	if item.ID != "work-new" || item.Name != "New work item" {
		t.Fatalf("unexpected item: %#v", item)
	}
}

func TestCreateIssueRequiresTitleAndConfig(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Token: "test-token"}, nil)
	_, err := client.CreateIssue(context.Background(), CreateIssueInput{
		WorkspaceSlug: "general",
		ProjectID:     "project-1",
		Title:         "  ",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty title: expected ErrInvalidInput, got %v", err)
	}

	_, err = client.CreateIssue(context.Background(), CreateIssueInput{
		Title: "New work item",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("missing workspace/project: expected ErrInvalidInput, got %v", err)
	}
}

func TestCreateWorkItemMapsNeutralCreateInput(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 != "/api/v1/workspaces/general/projects/project-1/work-items/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["name"] != "Neutral title" {
			t.Errorf("name: got %q", body["name"])
		}
		if body["description_html"] != "plain description" {
			t.Errorf("description_html: got %q", body["description_html"])
		}
		w.WriteHeader(http.StatusCreated)
		_ = json.NewEncoder(w).Encode(map[string]any{"id": "work-neutral"})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	result, err := client.CreateWorkItem(context.Background(), workitem.CreateInput{
		Provider:        "plane",
		Tenant:          "general",
		Project:         "project-1",
		Title:           "Neutral title",
		DescriptionText: "plain description",
	})
	if err != nil {
		t.Fatalf("CreateWorkItem returned error: %v", err)
	}
	if result.Ref.Provider != workitem.ProviderID("plane") {
		t.Errorf("ref.Provider: got %q", result.Ref.Provider)
	}
	if result.Ref.Tenant != "general" || result.Ref.Project != "project-1" {
		t.Errorf("ref tenant/project: got %q/%q", result.Ref.Tenant, result.Ref.Project)
	}
	if result.Ref.ID != "work-neutral" {
		t.Errorf("ref.ID: got %q", result.Ref.ID)
	}
}

func TestFetchWorkItemMapsNeutralRefToPlaneEndpoint(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 != "/api/v1/workspaces/general/projects/project-1/work-items/work-1" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":                   "work-1",
			"name":                 "Fix issue",
			"description_stripped": "Do the thing",
			"description_html":     "<p>Do the thing</p>",
		})
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	ref := workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"}
	item, err := client.FetchWorkItem(context.Background(), ref)
	if err != nil {
		t.Fatalf("FetchWorkItem returned error: %v", err)
	}
	if item.Title != "Fix issue" {
		t.Errorf("title: got %q", item.Title)
	}
	if item.DescriptionText != "Do the thing" {
		t.Errorf("description_text: got %q", item.DescriptionText)
	}
	if item.DescriptionHTML != "<p>Do the thing</p>" {
		t.Errorf("description_html: got %q", item.DescriptionHTML)
	}
	if item.Ref.ID != "work-1" {
		t.Errorf("ref.ID: got %q", item.Ref.ID)
	}
}

func TestAppendCommentMapsNeutralComment(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 != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/comments/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["comment_html"] != "<p>done</p>" {
			t.Errorf("comment_html: got %q", body["comment_html"])
		}
		if body["external_source"] != "nomadcode" {
			t.Errorf("external_source: got %q", body["external_source"])
		}
		if body["external_id"] != "task-1" {
			t.Errorf("external_id: got %q", body["external_id"])
		}
		if body["access"] != "INTERNAL" {
			t.Errorf("access: got %q", body["access"])
		}
		w.WriteHeader(http.StatusCreated)
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err := client.AppendComment(context.Background(), workitem.CommentInput{
		Ref:            workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"},
		Body:           "<p>done</p>",
		Format:         workitem.CommentFormatHTML,
		ExternalSource: "nomadcode",
		ExternalID:     "task-1",
		Visibility:     "INTERNAL",
	})
	if err != nil {
		t.Fatalf("AppendComment returned error: %v", err)
	}
}

func TestProjectStatusRequiresProviderState(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.com", Token: "test-token"}, nil)
	err := client.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:           workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"},
		ProviderState: "",
	})
	if err == nil || !strings.Contains(err.Error(), ErrInvalidInput.Error()) {
		t.Fatalf("empty provider state: expected ErrInvalidInput, got %v", err)
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch {
			t.Fatalf("method: got %s", r.Method)
		}
		if r.URL.Path != "/api/v1/workspaces/general/projects/project-1/work-items/work-1/" {
			t.Fatalf("path: got %s", r.URL.Path)
		}
		var body map[string]string
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		if body["state"] != "state-done" {
			t.Errorf("state: got %q", body["state"])
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	client2 := NewClient(Config{BaseURL: server.URL, Token: "test-token"}, nil)
	err = client2.ProjectStatus(context.Background(), workitem.StatusProjection{
		Ref:            workitem.Ref{Provider: "plane", Tenant: "general", Project: "project-1", ID: "work-1"},
		CanonicalState: "done",
		ProviderState:  "state-done",
	})
	if err != nil {
		t.Fatalf("ProjectStatus returned error: %v", err)
	}
}

func TestClientRequiresBaseURLAndToken(t *testing.T) {
	client := NewClient(Config{}, nil)
	_, err := client.GetWorkItem(context.Background(), WorkItemRef{
		WorkspaceSlug: "general",
		ProjectID:     "project-1",
		WorkItemID:    "work-1",
	})
	if err == nil || !strings.Contains(err.Error(), ErrMissingConfig.Error()) {
		t.Fatalf("expected missing config error, got %v", err)
	}
}

func TestFetchWorkItemMapsStateAndAssignees(t *testing.T) {
	// Test case 1: state as string, assignees as list of string
	server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":                   "work-1",
			"name":                 "Test 1",
			"description_stripped": "Desc",
			"state":                "state-active",
			"assignees":            []string{"user-a", "user-b"},
		})
	}))
	defer server1.Close()

	client1 := NewClient(Config{BaseURL: server1.URL, Token: "t"}, nil)
	item1, err := client1.FetchWorkItem(context.Background(), workitem.Ref{Provider: "plane", Tenant: "t", Project: "p", ID: "1"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if item1.State != "state-active" {
		t.Errorf("state: got %q, want 'state-active'", item1.State)
	}
	if len(item1.AssigneeIDs) != 2 || item1.AssigneeIDs[0] != "user-a" || item1.AssigneeIDs[1] != "user-b" {
		t.Errorf("assigneeIDs: got %v", item1.AssigneeIDs)
	}

	// Test case 2: state as object, assignees as list of objects
	server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":   "work-2",
			"name": "Test 2",
			"state": map[string]string{
				"id": "state-uuid",
			},
			"assignees": []map[string]string{
				{"id": "user-uuid-1"},
				{"id": "user-uuid-2"},
			},
		})
	}))
	defer server2.Close()

	client2 := NewClient(Config{BaseURL: server2.URL, Token: "t"}, nil)
	item2, err := client2.FetchWorkItem(context.Background(), workitem.Ref{Provider: "plane", Tenant: "t", Project: "p", ID: "2"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if item2.State != "state-uuid" {
		t.Errorf("state: got %q, want 'state-uuid'", item2.State)
	}
	if len(item2.AssigneeIDs) != 2 || item2.AssigneeIDs[0] != "user-uuid-1" || item2.AssigneeIDs[1] != "user-uuid-2" {
		t.Errorf("assigneeIDs: got %v", item2.AssigneeIDs)
	}

	// Test case 3: assignees as nested objects (e.g. assignee detail)
	server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":   "work-3",
			"name": "Test 3",
			"state_id": "state-direct",
			"assignees": []map[string]any{
				{"assignee": map[string]string{"id": "user-nested-1"}},
			},
		})
	}))
	defer server3.Close()

	client3 := NewClient(Config{BaseURL: server3.URL, Token: "t"}, nil)
	item3, err := client3.FetchWorkItem(context.Background(), workitem.Ref{Provider: "plane", Tenant: "t", Project: "p", ID: "3"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if item3.State != "state-direct" {
		t.Errorf("state: got %q, want 'state-direct'", item3.State)
	}
	if len(item3.AssigneeIDs) != 1 || item3.AssigneeIDs[0] != "user-nested-1" {
		t.Errorf("assigneeIDs: got %v", item3.AssigneeIDs)
	}
}
