package jira

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/alertstore/pkg/alert"
)

func testConfig(baseURL string) Config {
	return Config{
		BaseURL: baseURL,
		User:    "bot@pingcap.com",
		Token:   "token",
		Project: "O11Y",
	}
}

// Only states with a Jira meaning move the issue: an acked alert handed on
// returns to open, and moving it backwards would misrepresent it.
func TestTransitionForOnlyMapsMeaningfulStates(t *testing.T) {
	t.Parallel()

	require.Equal(t, "In Progress", TransitionFor(alert.StateAcked))
	require.Equal(t, "Done", TransitionFor(alert.StateResolved))
	require.Empty(t, TransitionFor(alert.StateOpen))
	require.Empty(t, TransitionFor(""))
}

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

	// A deployment missing any of the four essentials must write nothing.
	partial := Config{BaseURL: "https://tidb.atlassian.net", User: "bot", Project: "O11Y"}
	c := New(partial, nil)
	require.False(t, c.Enabled())
	// Rendering a browse link still works: it needs only base_url, and a key that
	// already exists should stay clickable even after writes are turned off.
	require.Equal(t, "https://tidb.atlassian.net/browse/O11Y-1", c.IssueURL("O11Y-1"))

	var nilClient *Client
	require.False(t, nilClient.Enabled())
	require.Empty(t, nilClient.IssueURL("O11Y-1"))
}

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

	c := New(testConfig("https://tidb.atlassian.net/"), nil)
	require.Equal(t, "https://tidb.atlassian.net/browse/O11Y-42", c.IssueURL("O11Y-42"))
	require.Empty(t, c.IssueURL(""))
}

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

	c := New(testConfig("https://tidb.atlassian.net/"), nil)
	got := c.IssuesSearchURL()
	require.Equal(t, "https://tidb.atlassian.net/issues/?jql=project+%3D+O11Y+AND+resolution+%3D+Unresolved", got)

	// Only base_url and project are needed to build the link — the same
	// "still clickable with writes off" contract IssueURL has.
	partial := Config{BaseURL: "https://tidb.atlassian.net", Project: "O11Y"}
	require.NotEmpty(t, New(partial, nil).IssuesSearchURL())

	require.Empty(t, New(Config{Project: "O11Y"}, nil).IssuesSearchURL(), "no base_url")
	require.Empty(t, New(Config{BaseURL: "https://tidb.atlassian.net"}, nil).IssuesSearchURL(), "no project")

	var nilClient *Client
	require.Empty(t, nilClient.IssuesSearchURL())
}

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

	var got map[string]interface{}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/rest/api/2/issue", r.URL.Path)
		user, token, ok := r.BasicAuth()
		require.True(t, ok)
		require.Equal(t, "bot@pingcap.com", user)
		require.Equal(t, "token", token)

		body, _ := io.ReadAll(r.Body)
		require.NoError(t, json.Unmarshal(body, &got))
		_, _ = w.Write([]byte(`{"key":"O11Y-42"}`))
	}))
	defer srv.Close()

	c := New(testConfig(srv.URL), nil)
	key, err := c.CreateIssue(context.Background(), &Issue{
		Project:      "O11Y",
		IssueType:    "Task",
		Summary:      "[PROD]TiKVDiskAlmostFull",
		Priority:     "Critical",
		Labels:       []string{"alertstore", "ALT-2607-42"},
		Components:   []string{"tikv"},
		CustomFields: map[string]interface{}{"customfield_10157": "1372813089206751289"},
	})
	require.NoError(t, err)
	require.Equal(t, "O11Y-42", key)

	fields := got["fields"].(map[string]interface{})
	require.Equal(t, "[PROD]TiKVDiskAlmostFull", fields["summary"])
	require.Equal(t, "O11Y", fields["project"].(map[string]interface{})["key"])
	require.Equal(t, "Critical", fields["priority"].(map[string]interface{})["name"])
	require.Equal(t, "1372813089206751289", fields["customfield_10157"])
	require.Equal(t, []interface{}{map[string]interface{}{"name": "tikv"}}, fields["components"])
}

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

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"errors":{"components":"Component name is required"}}`))
	}))
	defer srv.Close()

	_, err := New(testConfig(srv.URL), nil).CreateIssue(context.Background(), &Issue{Project: "O11Y"})
	require.ErrorIs(t, err, ErrPermanent)
}

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

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadGateway)
	}))
	defer srv.Close()

	_, err := New(testConfig(srv.URL), nil).CreateIssue(context.Background(), &Issue{Project: "O11Y"})
	require.Error(t, err)
	require.NotErrorIs(t, err, ErrPermanent)
}

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

	var posted map[string]interface{}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			_, _ = w.Write([]byte(`{"transitions":[
				{"id":"31","name":"Resolve this issue","to":{"name":"Done","statusCategory":{"key":"done"}}}
			]}`))
			return
		}
		body, _ := io.ReadAll(r.Body)
		require.NoError(t, json.Unmarshal(body, &posted))
		w.WriteHeader(http.StatusNoContent)
	}))
	defer srv.Close()

	// Naming the target status rather than the transition still moves it.
	moved, err := New(testConfig(srv.URL), nil).Transition(context.Background(), "O11Y-42", "Done")
	require.NoError(t, err)
	require.True(t, moved)
	require.Equal(t, "31", posted["transition"].(map[string]interface{})["id"])
}

// An issue already closed offers no transition to Done. That is an answer, not a
// failure, and the caller still wants to leave its comment.
func TestUnavailableTransitionIsNotAnError(t *testing.T) {
	t.Parallel()

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method, "must not attempt a transition it was not offered")
		_, _ = w.Write([]byte(`{"transitions":[]}`))
	}))
	defer srv.Close()

	moved, err := New(testConfig(srv.URL), nil).Transition(context.Background(), "O11Y-42", "Done")
	require.NoError(t, err)
	require.False(t, moved)
}

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

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("an unconfigured transition must not call Jira")
	}))
	defer srv.Close()

	moved, err := New(testConfig(srv.URL), nil).Transition(context.Background(), "O11Y-42", "")
	require.NoError(t, err)
	require.False(t, moved)
}
