package workitem

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
)

func TestNormalizeRefTrimsAndDefaultsMetadata(t *testing.T) {
	ref, err := NormalizeRef(Ref{
		Provider: "  plane  ",
		Tenant:   "  general  ",
		Project:  "  project-1  ",
		ID:       "  work-1  ",
		URL:      "  https://example.com  ",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if ref.Provider != "plane" {
		t.Errorf("provider: got %q", ref.Provider)
	}
	if ref.Tenant != "general" {
		t.Errorf("tenant: got %q", ref.Tenant)
	}
	if ref.Project != "project-1" {
		t.Errorf("project: got %q", ref.Project)
	}
	if ref.ID != "work-1" {
		t.Errorf("id: got %q", ref.ID)
	}
	if ref.URL != "https://example.com" {
		t.Errorf("url: got %q", ref.URL)
	}
	if string(ref.Metadata) != "{}" {
		t.Errorf("metadata: got %q", ref.Metadata)
	}
}

func TestNormalizeRefKeepsValidMetadata(t *testing.T) {
	meta := json.RawMessage(`{"key":"val"}`)
	ref, err := NormalizeRef(Ref{
		Provider: "plane",
		ID:       "work-1",
		Metadata: meta,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if string(ref.Metadata) != `{"key":"val"}` {
		t.Errorf("metadata: got %q", ref.Metadata)
	}
}

func TestNormalizeRefRequiresProviderAndID(t *testing.T) {
	_, err := NormalizeRef(Ref{Provider: "", ID: "work-1"})
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("empty provider: expected ErrInvalidRef, got %v", err)
	}

	_, err = NormalizeRef(Ref{Provider: "plane", ID: ""})
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("empty id: expected ErrInvalidRef, got %v", err)
	}

	_, err = NormalizeRef(Ref{Provider: "  ", ID: "  "})
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("whitespace-only: expected ErrInvalidRef, got %v", err)
	}
}

func TestNormalizeRefRejectsInvalidMetadata(t *testing.T) {
	_, err := NormalizeRef(Ref{
		Provider: "plane",
		ID:       "work-1",
		Metadata: json.RawMessage("not-json"),
	})
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("expected ErrInvalidRef, got %v", err)
	}
}

func TestMappingReturnsTrimmedProviderTargets(t *testing.T) {
	m := Mapping{
		States: map[string]string{
			"done":  "  state-done  ",
			"empty": "",
		},
		Labels: map[string]string{
			"bug":   "  label-bug  ",
			"empty": "",
		},
	}

	target, ok := m.StateTarget("done")
	if !ok || target != "state-done" {
		t.Errorf("StateTarget done: got %q, ok=%v", target, ok)
	}
	_, ok = m.StateTarget("empty")
	if ok {
		t.Error("StateTarget empty: expected false")
	}
	_, ok = m.StateTarget("missing")
	if ok {
		t.Error("StateTarget missing: expected false")
	}

	target, ok = m.LabelTarget("bug")
	if !ok || target != "label-bug" {
		t.Errorf("LabelTarget bug: got %q, ok=%v", target, ok)
	}
	_, ok = m.LabelTarget("empty")
	if ok {
		t.Error("LabelTarget empty: expected false")
	}
	_, ok = m.LabelTarget("missing")
	if ok {
		t.Error("LabelTarget missing: expected false")
	}
}

func TestCapabilityCreateSupportsLookupIndependently(t *testing.T) {
	caps := Capabilities{
		CapabilityLookup: true,
		CapabilityCreate: true,
	}
	if !caps.Supports(CapabilityCreate) {
		t.Error("expected create capability to be supported")
	}
	if caps.Supports(CapabilityComment) {
		t.Error("comment capability should be unset")
	}
	if string(CapabilityCreate) != "create" {
		t.Errorf("CapabilityCreate value: got %q", CapabilityCreate)
	}
}

func TestCreatorInterfaceShapeIsStable(t *testing.T) {
	// Compile-time guard: a Creator must accept CreateInput and return CreateResult.
	var _ Creator = createStub{}
}

type createStub struct{}

func (createStub) CreateWorkItem(_ context.Context, input CreateInput) (CreateResult, error) {
	return CreateResult{Ref: Ref{Provider: input.Provider, ID: "stub"}}, nil
}

func TestBuildCreateTaskInputUsesCommentBeforeDescription(t *testing.T) {
	input := TaskCreateInput{
		Ref:     Ref{Provider: "plane", ID: "work-1", Tenant: "acme", Project: "proj-1"},
		StateID: "state-abc",
		Comment: "my comment",
	}
	item := WorkItem{
		Ref:             input.Ref,
		Title:           "Issue Title",
		DescriptionText: "description text",
	}

	out, err := BuildCreateTaskInput(input, item)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out.Title != "Issue Title" {
		t.Errorf("title: got %q", out.Title)
	}
	if out.Source != "plane" {
		t.Errorf("source: got %q", out.Source)
	}
	if out.External == nil || out.External.Provider != "plane" {
		t.Errorf("external.provider: got %v", out.External)
	}

	var payload map[string]any
	if err := json.Unmarshal(out.Payload, &payload); err != nil {
		t.Fatalf("payload unmarshal: %v", err)
	}
	if payload["message"] != "my comment" {
		t.Errorf("message: got %v, want 'my comment'", payload["message"])
	}
}

func TestBuildCreateTaskInputStoresProviderNeutralMetadata(t *testing.T) {
	input := TaskCreateInput{
		Ref:     Ref{Provider: "plane", ID: "work-2", Tenant: "acme", Project: "proj-2", URL: "https://example.com/work/2"},
		StateID: "  state-xyz  ",
	}
	item := WorkItem{
		Ref:   input.Ref,
		Title: "Task",
	}

	out, err := BuildCreateTaskInput(input, item)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	var payload map[string]any
	if err := json.Unmarshal(out.Payload, &payload); err != nil {
		t.Fatalf("payload unmarshal: %v", err)
	}
	wi, ok := payload["work_item"].(map[string]any)
	if !ok {
		t.Fatalf("work_item missing or wrong type: %v", payload["work_item"])
	}

	checks := map[string]string{
		"provider":     "plane",
		"tenant":       "acme",
		"project":      "proj-2",
		"id":           "work-2",
		"state_id":     "state-xyz",
		"external_url": "https://example.com/work/2",
	}
	for k, want := range checks {
		if wi[k] != want {
			t.Errorf("work_item[%q]: got %v, want %q", k, wi[k], want)
		}
	}

	var extMeta map[string]any
	if err := json.Unmarshal(out.External.Metadata, &extMeta); err != nil {
		t.Fatalf("external metadata unmarshal: %v", err)
	}
	for k, want := range checks {
		if extMeta[k] != want {
			t.Errorf("external.metadata[%q]: got %v, want %q", k, extMeta[k], want)
		}
	}
}

func TestBuildCreateTaskInputSkipsWhitespaceMessageCandidates(t *testing.T) {
	ref := Ref{Provider: "plane", ID: "work-4", Tenant: "acme", Project: "proj-1"}

	// whitespace comment -> falls back to DescriptionText, which is trimmed
	out, err := BuildCreateTaskInput(
		TaskCreateInput{Ref: ref, Comment: "   "},
		WorkItem{Ref: ref, Title: "T", DescriptionText: "  description text  "},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	var payload map[string]any
	if err := json.Unmarshal(out.Payload, &payload); err != nil {
		t.Fatalf("payload unmarshal: %v", err)
	}
	if payload["message"] != "description text" {
		t.Errorf("message: got %v, want 'description text'", payload["message"])
	}

	// comment and DescriptionText both whitespace -> falls back to DescriptionHTML
	out, err = BuildCreateTaskInput(
		TaskCreateInput{Ref: ref, Comment: "  "},
		WorkItem{Ref: ref, Title: "T", DescriptionText: "  ", DescriptionHTML: "<p>html</p>"},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if err := json.Unmarshal(out.Payload, &payload); err != nil {
		t.Fatalf("payload unmarshal: %v", err)
	}
	if payload["message"] != "<p>html</p>" {
		t.Errorf("message: got %v, want '<p>html</p>'", payload["message"])
	}

	// all candidates whitespace -> falls back to title (trimmed)
	out, err = BuildCreateTaskInput(
		TaskCreateInput{Ref: ref, Comment: "  "},
		WorkItem{Ref: ref, Title: "  My Title  ", DescriptionText: " ", DescriptionHTML: "  "},
	)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if err := json.Unmarshal(out.Payload, &payload); err != nil {
		t.Fatalf("payload unmarshal: %v", err)
	}
	if payload["message"] != "My Title" {
		t.Errorf("message: got %v, want 'My Title'", payload["message"])
	}
}

func TestBuildCreateTaskInputFallsBackToRefIDAndRequiresValidRef(t *testing.T) {
	input := TaskCreateInput{
		Ref: Ref{Provider: "plane", ID: "work-3"},
	}
	item := WorkItem{
		Ref:   input.Ref,
		Title: "  ",
	}

	out, err := BuildCreateTaskInput(input, item)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out.Title != "work-3" {
		t.Errorf("title fallback: got %q, want 'work-3'", out.Title)
	}

	_, err = BuildCreateTaskInput(TaskCreateInput{Ref: Ref{Provider: "", ID: "work-3"}}, item)
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("missing provider: expected ErrInvalidRef, got %v", err)
	}

	_, err = BuildCreateTaskInput(TaskCreateInput{Ref: Ref{Provider: "plane", ID: ""}}, item)
	if !errors.Is(err, ErrInvalidRef) {
		t.Errorf("missing id: expected ErrInvalidRef, got %v", err)
	}
}

func TestWorkItemHasAssignee(t *testing.T) {
	item := WorkItem{
		AssigneeIDs: []string{"  user-1  ", "user-2"},
	}

	if !item.HasAssignee("user-1") {
		t.Error("expected to find user-1")
	}
	if !item.HasAssignee("  user-2  ") {
		t.Error("expected to find user-2 with trimmed spaces")
	}
	if item.HasAssignee("user-3") {
		t.Error("did not expect to find user-3")
	}
	if item.HasAssignee("") {
		t.Error("did not expect to find empty assignee")
	}
}
