// topics_test.go contains unit tests for the topic MCP tool handlers.
// Tests use httptest to mock GitLab API responses and verify success, error,
// and edge-case paths.
package topics

import (
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/jmrplens/gitlab-mcp-server/v2/internal/testutil"
	"github.com/jmrplens/gitlab-mcp-server/v2/internal/toolutil"
)

// errExpNonNilResult identifies the err exp non nil result constant used by this package.
const errExpNonNilResult = "expected non-nil result"

// errNoReachAPI identifies the err no reach API constant used by this package.
const errNoReachAPI = "should not reach API"

// fmtUnexpErr identifies the fmt unexp err constant used by this package.
const fmtUnexpErr = "unexpected error: %v"

// topicJSON identifies the topic JSON constant used by this package.
const topicJSON = `{"id":1,"name":"go","title":"Go","description":"The Go programming language","total_projects_count":42,"avatar_url":"https://example.com/go.png"}`

// pathTopics identifies the path topics constant used by this package.
const pathTopics = "/api/v4/topics"

// pathTopicOne identifies the path topic one constant used by this package.
const pathTopicOne = "/api/v4/topics/1"

// errExpErrZeroTopicID identifies the err exp err zero topic ID constant used by this package.
const errExpErrZeroTopicID = "expected error for zero topic_id"

// errExpErrNegTopicID identifies the err exp err neg topic ID constant used by this package.
const errExpErrNegTopicID = "expected error for negative topic_id"

// testTopicID identifies the test topic ID constant used by this package.
const testTopicID = "topic_id"

// fmtExpErrMentionTopicID identifies the fmt exp err mention topic ID constant used by this package.
const fmtExpErrMentionTopicID = "expected error to mention topic_id, got %q"

// TestList_Success verifies List when success.
func TestList_Success(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopics && r.Method == http.MethodGet {
			testutil.RespondJSON(w, http.StatusOK, `[`+topicJSON+`]`)
			return
		}
		http.NotFound(w, r)
	}))

	out, err := List(t.Context(), client, ListInput{})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
	if len(out.Topics) != 1 {
		t.Fatalf("expected 1 topic, got %d", len(out.Topics))
	}
	if out.Topics[0].Name != "go" {
		t.Errorf("expected name 'go', got %q", out.Topics[0].Name)
	}
	if out.Topics[0].TotalProjectsCount != 42 {
		t.Errorf("expected 42 projects, got %d", out.Topics[0].TotalProjectsCount)
	}
}

// TestList_WithSearch verifies List when with search.
func TestList_WithSearch(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopics && r.URL.Query().Get("search") == "go" {
			testutil.RespondJSON(w, http.StatusOK, `[`+topicJSON+`]`)
			return
		}
		http.NotFound(w, r)
	}))

	out, err := List(t.Context(), client, ListInput{Search: "go"})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
	if len(out.Topics) != 1 {
		t.Fatalf("expected 1 topic, got %d", len(out.Topics))
	}
}

// TestList_Error verifies List when error.
func TestList_Error(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusForbidden)
	}))

	_, err := List(t.Context(), client, ListInput{})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

// TestGet_Success verifies Get when success.
func TestGet_Success(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopicOne && r.Method == http.MethodGet {
			testutil.RespondJSON(w, http.StatusOK, topicJSON)
			return
		}
		http.NotFound(w, r)
	}))

	out, err := Get(t.Context(), client, GetInput{TopicID: 1})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
	if out.Topic.ID != 1 {
		t.Errorf("expected topic ID 1, got %d", out.Topic.ID)
	}
	if out.Topic.Title != "Go" {
		t.Errorf("expected title 'Go', got %q", out.Topic.Title)
	}
}

// TestCreate_Success verifies Create when success.
func TestCreate_Success(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopics && r.Method == http.MethodPost {
			testutil.RespondJSON(w, http.StatusCreated, topicJSON)
			return
		}
		http.NotFound(w, r)
	}))

	out, err := Create(t.Context(), client, CreateInput{Name: "go", Title: "Go"})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
	if out.Topic.Name != "go" {
		t.Errorf("expected name 'go', got %q", out.Topic.Name)
	}
}

// TestUpdate_Success verifies Update when success.
func TestUpdate_Success(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopicOne && r.Method == http.MethodPut {
			testutil.RespondJSON(w, http.StatusOK, topicJSON)
			return
		}
		http.NotFound(w, r)
	}))

	out, err := Update(t.Context(), client, UpdateInput{TopicID: 1, Title: "Golang"})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
	if out.Topic.ID != 1 {
		t.Errorf("expected topic ID 1, got %d", out.Topic.ID)
	}
}

// TestDelete_Success verifies Delete when success.
func TestDelete_Success(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == pathTopicOne && r.Method == http.MethodDelete {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		http.NotFound(w, r)
	}))

	err := Delete(t.Context(), client, DeleteInput{TopicID: 1})
	if err != nil {
		t.Fatalf(fmtUnexpErr, err)
	}
}

// TestDelete_Error verifies Delete when error.
func TestDelete_Error(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusForbidden)
	}))

	err := Delete(t.Context(), client, DeleteInput{TopicID: 1})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

// TestGet_InvalidTopicID verifies Get when invalid topic ID.
func TestGet_InvalidTopicID(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		t.Fatal(errNoReachAPI)
	}))
	_, err := Get(t.Context(), client, GetInput{TopicID: 0})
	if err == nil {
		t.Fatal(errExpErrZeroTopicID)
	}
	if !strings.Contains(err.Error(), testTopicID) {
		t.Errorf(fmtExpErrMentionTopicID, err)
	}
	_, err = Get(t.Context(), client, GetInput{TopicID: -1})
	if err == nil {
		t.Fatal(errExpErrNegTopicID)
	}
}

// TestUpdate_InvalidTopicID verifies Update when invalid topic ID.
func TestUpdate_InvalidTopicID(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		t.Fatal(errNoReachAPI)
	}))
	_, err := Update(t.Context(), client, UpdateInput{TopicID: 0, Title: "x"})
	if err == nil {
		t.Fatal(errExpErrZeroTopicID)
	}
	if !strings.Contains(err.Error(), testTopicID) {
		t.Errorf(fmtExpErrMentionTopicID, err)
	}
	_, err = Update(t.Context(), client, UpdateInput{TopicID: -1, Title: "x"})
	if err == nil {
		t.Fatal(errExpErrNegTopicID)
	}
}

// TestDelete_InvalidTopicID verifies Delete when invalid topic ID.
func TestDelete_InvalidTopicID(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		t.Fatal(errNoReachAPI)
	}))
	err := Delete(t.Context(), client, DeleteInput{TopicID: 0})
	if err == nil {
		t.Fatal(errExpErrZeroTopicID)
	}
	if !strings.Contains(err.Error(), testTopicID) {
		t.Errorf(fmtExpErrMentionTopicID, err)
	}
	err = Delete(t.Context(), client, DeleteInput{TopicID: -1})
	if err == nil {
		t.Fatal(errExpErrNegTopicID)
	}
}

// TestFormatListMarkdown_Empty verifies FormatListMarkdown when empty.
func TestFormatListMarkdown_Empty(t *testing.T) {
	result := FormatListMarkdown(ListOutput{})
	if result == nil {
		t.Fatal(errExpNonNilResult)
	}
	text := result.Content[0].(*mcp.TextContent).Text
	if !strings.Contains(text, "No topics") {
		t.Errorf("expected 'No topics' message, got %q", text)
	}
}

// TestFormatListMarkdown_WithData verifies FormatListMarkdown when with data.
func TestFormatListMarkdown_WithData(t *testing.T) {
	result := FormatListMarkdown(ListOutput{
		Topics: []TopicItem{
			{ID: 1, Name: "go", Title: "Go", TotalProjectsCount: 42},
		},
		Pagination: toolutil.PaginationOutput{},
	})
	if result == nil {
		t.Fatal(errExpNonNilResult)
	}
}

// TestFormatTopicMarkdown verifies FormatTopicMarkdown.
func TestFormatTopicMarkdown(t *testing.T) {
	result := FormatTopicMarkdown(TopicItem{
		ID: 1, Name: "go", Title: "Go", Description: "The Go language",
		TotalProjectsCount: 42, AvatarURL: "https://example.com/go.png",
	})
	if result == nil {
		t.Fatal(errExpNonNilResult)
	}
	text := result.Content[0].(*mcp.TextContent).Text
	if !strings.Contains(text, "go") {
		t.Errorf("expected topic name in output, got %q", text)
	}
}

// ---------- Tests consolidated from coverage_test.go ----------.

// errExpectedNil identifies the err expected nil constant used by this package.
const errExpectedNil = "expected error, got nil"

// ---------------------------------------------------------------------------
// List — API error (400)
// ---------------------------------------------------------------------------.

// TestList_APIError400 verifies List when API error 400.
func TestList_APIError400(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		testutil.RespondJSON(w, http.StatusBadRequest, `{"message":msgBadRequest}`)
	}))
	_, err := List(t.Context(), client, ListInput{})
	if err == nil {
		t.Fatal(errExpectedNil)
	}
}

// ---------------------------------------------------------------------------
// Get — API error (400)
// ---------------------------------------------------------------------------.

// TestGet_APIError400 verifies Get when API error 400.
func TestGet_APIError400(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		testutil.RespondJSON(w, http.StatusBadRequest, `{"message":msgBadRequest}`)
	}))
	_, err := Get(t.Context(), client, GetInput{TopicID: 1})
	if err == nil {
		t.Fatal(errExpectedNil)
	}
}

// ---------------------------------------------------------------------------
// Create — API error (400), with optional fields
// ---------------------------------------------------------------------------.

// TestCreate_APIError400 verifies Create when API error 400.
func TestCreate_APIError400(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		testutil.RespondJSON(w, http.StatusBadRequest, `{"message":msgBadRequest}`)
	}))
	_, err := Create(t.Context(), client, CreateInput{Name: "test"})
	if err == nil {
		t.Fatal(errExpectedNil)
	}
}

// TestCreate_WithAllOptionalFields verifies Create when with all optional fields.
func TestCreate_WithAllOptionalFields(t *testing.T) {
	var capturedBody string
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPost {
			body, err := io.ReadAll(r.Body)
			if err != nil {
				t.Fatalf("read request body: %v", err)
			}
			capturedBody = string(body)
			testutil.RespondJSON(w, http.StatusCreated, topicJSON)
			return
		}
		http.NotFound(w, r)
	}))
	out, err := Create(t.Context(), client, CreateInput{
		Name: "go", Title: "Go", Description: "The Go programming language",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out.Topic.Name != "go" {
		t.Errorf("expected name 'go', got %q", out.Topic.Name)
	}
	for _, want := range []string{"title", "description"} {
		if !strings.Contains(capturedBody, want) {
			t.Errorf("request body missing field %q", want)
		}
	}
}

// ---------------------------------------------------------------------------
// Update — API error (400), with all optional fields
// ---------------------------------------------------------------------------.

// TestUpdate_APIError400 verifies Update when API error 400.
func TestUpdate_APIError400(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		testutil.RespondJSON(w, http.StatusBadRequest, `{"message":msgBadRequest}`)
	}))
	_, err := Update(t.Context(), client, UpdateInput{TopicID: 1, Name: "x"})
	if err == nil {
		t.Fatal(errExpectedNil)
	}
}

// TestUpdate_WithAllOptionalFields verifies Update when with all optional fields.
func TestUpdate_WithAllOptionalFields(t *testing.T) {
	var capturedBody string
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPut {
			body, err := io.ReadAll(r.Body)
			if err != nil {
				t.Fatalf("read request body: %v", err)
			}
			capturedBody = string(body)
			testutil.RespondJSON(w, http.StatusOK, topicJSON)
			return
		}
		http.NotFound(w, r)
	}))
	out, err := Update(t.Context(), client, UpdateInput{
		TopicID: 1, Name: "golang", Title: "Golang", Description: "Updated desc",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out.Topic.ID != 1 {
		t.Errorf("expected topic ID 1, got %d", out.Topic.ID)
	}
	for _, want := range []string{"title", "description"} {
		if !strings.Contains(capturedBody, want) {
			t.Errorf("request body missing field %q", want)
		}
	}
}

// ---------------------------------------------------------------------------
// Delete — API error (400)
// ---------------------------------------------------------------------------.

// TestDelete_APIError400 verifies Delete when API error 400.
func TestDelete_APIError400(t *testing.T) {
	client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		testutil.RespondJSON(w, http.StatusBadRequest, `{"message":msgBadRequest}`)
	}))
	err := Delete(t.Context(), client, DeleteInput{TopicID: 1})
	if err == nil {
		t.Fatal(errExpectedNil)
	}
}

// ---------------------------------------------------------------------------
// Formatters — additional branches
// ---------------------------------------------------------------------------.

// TestFormatTopicMarkdown_MinimalFields verifies FormatTopicMarkdown when minimal fields.
func TestFormatTopicMarkdown_MinimalFields(t *testing.T) {
	result := FormatTopicMarkdown(TopicItem{ID: 1, Name: "test"})
	if result == nil {
		t.Fatal("expected non-nil result")
	}
	text := result.Content[0].(*mcp.TextContent).Text
	if strings.Contains(text, "Title") {
		t.Error("should not contain Title for empty title")
	}
	if strings.Contains(text, "Description") {
		t.Error("should not contain Description for empty description")
	}
	if strings.Contains(text, "Avatar") {
		t.Error("should not contain Avatar for empty avatar URL")
	}
}
