package client

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

	gojira "github.com/andygrunwald/go-jira"
	"github.com/stretchr/testify/require"
	"github.com/trivago/tgo/tcontainer"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"
)

func TestJiraClientUpdateAssignee(t *testing.T) {
	t.Parallel()

	var gotBody map[string]any

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/rest/api/2/user/search":
			require.Equal(t, http.MethodGet, r.Method)
			require.Equal(t, "alice@example.com", r.URL.Query().Get("query"))
			w.Header().Set("Content-Type", "application/json")
			_, err := w.Write([]byte(`[
				{"accountId":"customer-account","accountType":"customer","emailAddress":"alice@example.com"},
				{"accountId":"alice-account","accountType":"atlassian","emailAddress":"Alice@example.com"}
			]`))
			require.NoError(t, err)
		case "/rest/api/2/issue/DEMO-1/assignee":
			require.Equal(t, http.MethodPut, r.Method)
			require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody))
			w.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(w, r)
		}
	}))
	t.Cleanup(server.Close)

	client, err := NewJiraClient(config.JiraClient{
		URL:      server.URL,
		Username: "jira-user",
		Password: "jira-token",
	})
	require.NoError(t, err)

	err = client.UpdateAssignee(context.Background(), "DEMO-1", "alice@example.com")
	require.NoError(t, err)
	require.Equal(t, "alice-account", gotBody["accountId"])
	require.NotContains(t, gotBody, "name")
}

func TestJiraClientUpdateAssigneeReturnsErrorWhenUserNotFound(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/rest/api/2/user/search", r.URL.Path)
		w.Header().Set("Content-Type", "application/json")
		_, err := w.Write([]byte(`[
			{"accountId":"customer-account","accountType":"customer","emailAddress":"alice@example.com"},
			{"accountId":"other-account","accountType":"atlassian","emailAddress":"other@example.com"}
		]`))
		require.NoError(t, err)
	}))
	t.Cleanup(server.Close)

	client, err := NewJiraClient(config.JiraClient{
		URL:      server.URL,
		Username: "jira-user",
		Password: "jira-token",
	})
	require.NoError(t, err)

	err = client.UpdateAssignee(context.Background(), "DEMO-1", "alice@example.com")
	require.EqualError(t, err, "Jira user alice@example.com not found")
}

func TestJiraClientListsProjectComponents(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/rest/api/2/project/TCID/components", r.URL.Path)
		w.Header().Set("Content-Type", "application/json")
		_, err := w.Write([]byte(`[{"id":"1","name":"TiKV","description":"storage"}]`))
		require.NoError(t, err)
	}))
	t.Cleanup(server.Close)
	client, err := NewJiraClient(config.JiraClient{URL: server.URL, Username: "jira-user", Password: "jira-token"})
	require.NoError(t, err)

	components, err := client.ListProjectComponents(context.Background(), "TCID")

	require.NoError(t, err)
	require.Equal(t, []ProjectComponent{{ID: "1", Name: "TiKV", Description: "storage"}}, components)
}

func TestJiraClientListsIssueChangelogs(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/rest/api/3/issue/TCID-1/changelog", r.URL.Path)
		require.Equal(t, "25", r.URL.Query().Get("startAt"))
		require.Equal(t, "50", r.URL.Query().Get("maxResults"))
		w.Header().Set("Content-Type", "application/json")
		_, err := w.Write([]byte(`{"startAt":25,"maxResults":50,"total":26,"values":[{"id":"100","created":"2026-07-30T10:00:00.000+0800","author":{"accountId":"actor-1","displayName":"User"},"items":[{"field":"assignee","fieldId":"assignee","from":null,"fromString":null,"to":"account-1","toString":"Alice"}]}]}`))
		require.NoError(t, err)
	}))
	t.Cleanup(server.Close)
	client, err := NewJiraClient(config.JiraClient{URL: server.URL, Username: "jira-user", Password: "jira-token"})
	require.NoError(t, err)

	result, err := client.ListIssueChangelogs(context.Background(), "TCID-1", &ListIssueChangelogsOptions{StartAt: 25, MaxResults: 50})

	require.NoError(t, err)
	require.True(t, result.IsLast)
	require.Len(t, result.Values, 1)
	require.Equal(t, "assignee", result.Values[0].Items[0].FieldID)
	require.Equal(t, "account-1", result.Values[0].Items[0].To)
}

func TestJiraClientReturnsChangelogAPIError(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "Jira unavailable", http.StatusServiceUnavailable)
	}))
	t.Cleanup(server.Close)
	client, err := NewJiraClient(config.JiraClient{URL: server.URL, Username: "jira-user", Password: "jira-token"})
	require.NoError(t, err)

	_, err = client.ListIssueChangelogs(context.Background(), "TCID-1", nil)

	require.ErrorContains(t, err, "503 Service Unavailable")
}

func TestNewJiraClientDummyNoopsMutations(t *testing.T) {
	t.Parallel()

	client, err := NewJiraClient(config.JiraClient{Dummy: true})
	require.NoError(t, err)

	require.NoError(t, client.UpdateAssignee(context.Background(), "DEMO-1", "alice"))
	require.NoError(t, client.UpdateIssueStatus(context.Background(), "DEMO-1", "waiting for customer"))
}

func TestNewJiraClientDefaultCustomFieldSet(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(server.Close)

	client, err := NewJiraClient(config.JiraClient{
		URL:      server.URL,
		Username: "jira-user",
		Password: "jira-token",
	})
	require.NoError(t, err)
	require.Equal(t, TIDB_JIRA_CUSTOM_FIELD_SET, client.CustomFieldSet())
}

func TestNewJiraClientDummyDefaultCustomFieldSet(t *testing.T) {
	t.Parallel()

	client, err := NewJiraClient(config.JiraClient{Dummy: true})
	require.NoError(t, err)

	require.Equal(t, TIDB_JIRA_CUSTOM_FIELD_SET, client.CustomFieldSet())
}

func TestJiraClientEscalationOperations(t *testing.T) {
	t.Parallel()

	var requests []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests = append(requests, r.Method+" "+r.URL.Path)
		switch r.URL.Path {
		case "/rest/api/2/issue":
			var body map[string]interface{}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			fields := body["fields"].(map[string]interface{})
			require.Equal(t, "GTOC", fields["project"].(map[string]interface{})["key"])
			require.Equal(t, "10029", fields["issuetype"].(map[string]interface{})["id"])
			require.Equal(t, "APID-1", fields["customfield_source"])
			_, _ = w.Write([]byte(`{"key":"GTOC-1"}`))
		case "/rest/api/2/issue/APID-1":
			var body map[string]interface{}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			labels := body["update"].(map[string]interface{})["labels"].([]interface{})
			require.Equal(t, "Escalated", labels[0].(map[string]interface{})["add"])
			w.WriteHeader(http.StatusNoContent)
		case "/rest/servicedeskapi/request/APID-1/comment":
			var body map[string]interface{}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			require.Equal(t, "Escalation successful.", body["body"])
			require.Equal(t, false, body["public"])
			_, _ = w.Write([]byte(`{}`))
		case "/rest/api/2/issueLink":
			var body map[string]interface{}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			require.Equal(t, "10007", body["type"].(map[string]interface{})["id"])
			require.Equal(t, "GTOC-1", body["outwardIssue"].(map[string]interface{})["key"])
			require.Equal(t, "APID-1", body["inwardIssue"].(map[string]interface{})["key"])
			w.WriteHeader(http.StatusCreated)
		case "/rest/api/3/attachment/content/10":
			_, _ = w.Write([]byte("attachment data"))
		case "/rest/api/2/issue/GTOC-1/attachments":
			require.Equal(t, "no-check", r.Header.Get("X-Atlassian-Token"))
			require.NoError(t, r.ParseMultipartForm(1024))
			file, header, err := r.FormFile("file")
			require.NoError(t, err)
			defer file.Close()
			content, err := io.ReadAll(file)
			require.NoError(t, err)
			require.Equal(t, "diagnostic.txt", header.Filename)
			require.Equal(t, "attachment data", string(content))
			_, _ = w.Write([]byte(`[]`))
		default:
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
	}))
	t.Cleanup(server.Close)

	client, err := NewJiraClient(config.JiraClient{URL: server.URL, Username: "jira-user", Password: "jira-token"})
	require.NoError(t, err)
	created, err := client.CreateIssue(context.Background(), &gojira.Issue{Fields: &gojira.IssueFields{
		Summary: "summary", Project: gojira.Project{Key: "GTOC"}, Type: gojira.IssueType{ID: "10029"},
		Unknowns: tcontainer.MarshalMap{"customfield_source": "APID-1"},
	}})
	require.NoError(t, err)
	require.Equal(t, "GTOC-1", created.Key)
	require.NoError(t, client.AddIssueLabel(context.Background(), "APID-1", "Escalated"))
	require.NoError(t, client.AddInternalComment(context.Background(), "APID-1", "Escalation successful."))
	require.NoError(t, client.CreateIssueLink(context.Background(), "10007", "GTOC-1", "APID-1"))
	content, err := client.DownloadAttachment(context.Background(), "10")
	require.NoError(t, err)
	require.NoError(t, client.UploadAttachment(context.Background(), "GTOC-1", "diagnostic.txt", content))
	require.NoError(t, content.Close())
	require.Equal(t, []string{
		"POST /rest/api/2/issue",
		"PUT /rest/api/2/issue/APID-1",
		"POST /rest/servicedeskapi/request/APID-1/comment",
		"POST /rest/api/2/issueLink",
		"GET /rest/api/3/attachment/content/10",
		"POST /rest/api/2/issue/GTOC-1/attachments",
	}, requests)
}

func TestDummyJiraClientEscalationMutations(t *testing.T) {
	t.Parallel()

	client, err := NewJiraClient(config.JiraClient{Dummy: true})
	require.NoError(t, err)
	require.NoError(t, client.AddIssueLabel(context.Background(), "APID-1", "Escalated"))
	require.NoError(t, client.AddInternalComment(context.Background(), "APID-1", "comment"))
	require.NoError(t, client.CreateIssueLink(context.Background(), "10007", "GTOC-1", "APID-1"))
	require.NoError(t, client.UploadAttachment(context.Background(), "GTOC-1", "file.txt", strings.NewReader("data")))
}
