package advice

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/aisupport/apps/cli/ticket-manager/common"
	baseapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/base"
	wsapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/ticket-manager/workspace"
	"github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/base"
	wsent "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/ticket-manager/workspace"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/auth/consts"
)

func TestListBuildsWorkspaceAdviceRequest(t *testing.T) {
	var called bool
	server := setupServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/api/v1/ticketmanager/workspace/advices", r.URL.Path)
		require.Equal(t, "1001", r.URL.Query().Get("workspace_id"))
		require.Equal(t, []string{"add_task", "bind_lark_meeting", "bind_lark_doc"}, r.URL.Query()["type"])
		require.Equal(t, "2", r.URL.Query().Get("page"))
		require.Equal(t, "50", r.URL.Query().Get("page_size"))
		writeResponse(t, w, &wsapi.ListAdvicesResponse{
			BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
			Advices:      []*wsent.Advice{testAdvice()},
			Total:        1,
		})
	}))

	setupClient(t, server.URL)
	var stdout bytes.Buffer
	setupPrinter(t, &stdout)
	cmd := NewCommand()
	cmd.SetArgs([]string{
		"list",
		"--workspace-id",
		"1001",
		"--type",
		"add_task",
		"--type",
		"bind_lark_meeting",
		"--type",
		"bind_lark_doc",
		"--page",
		"2",
		"--page-size",
		"50",
	})

	err := cmd.Execute()
	require.NoError(t, err)
	require.True(t, called)
	require.Contains(t, stdout.String(), `"workspace_id": 1001`)
	require.Contains(t, stdout.String(), `"type": "add_task"`)
}

func TestGetBuildsWorkspaceAdviceRequest(t *testing.T) {
	var called bool
	server := setupServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/api/v1/ticketmanager/workspace/advices/7001", r.URL.Path)
		writeResponse(t, w, &wsapi.GetAdviceResponse{
			BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
			Advice:       testAdvice(),
		})
	}))

	setupClient(t, server.URL)
	var stdout bytes.Buffer
	setupPrinter(t, &stdout)
	cmd := NewCommand()
	cmd.SetArgs([]string{"get", "7001"})

	err := cmd.Execute()
	require.NoError(t, err)
	require.True(t, called)
	require.Contains(t, stdout.String(), `"id": 7001`)
}

func TestCreateBuildsWorkspaceAdviceRequest(t *testing.T) {
	var req wsapi.CreateAdviceRequest
	server := setupServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/api/v1/ticketmanager/workspace/advices", r.URL.Path)
		require.Equal(t, "application/json", r.Header.Get("Content-Type"))
		require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
		writeResponse(t, w, &wsapi.CreateAdviceResponse{
			BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
			Advice:       testAdvice(),
		})
	}))

	setupClient(t, server.URL)
	var stdout bytes.Buffer
	setupPrinter(t, &stdout)
	cmd := NewCommand()
	cmd.SetArgs([]string{
		"create",
		"--workspace-id",
		"1001",
		"--description",
		"create task",
		"--type",
		"add_task",
		"--data",
		`{"add_task":{"title":"task title"}}`,
		"--reply-message-id",
		"om_source_message",
	})

	err := cmd.Execute()
	require.NoError(t, err)
	require.Equal(t, uint64(1001), req.WorkspaceID)
	require.Equal(t, "create task", req.Description)
	require.Equal(t, wsent.AdivceType(wsent.AdivceTypeAddTask), req.Type)
	require.Equal(t, wsent.AdviceData{AddTask: &wsent.AdviceDataAddTask{Title: "task title"}}, req.Data)
	require.Equal(t, "om_source_message", req.ReplyMessageID)
	require.Contains(t, stdout.String(), `"description": "add task"`)
}

func TestUpdateBuildsWorkspaceAdviceRequestWithDataFile(t *testing.T) {
	dataPath := filepath.Join(t.TempDir(), "data.json")
	require.NoError(t, os.WriteFile(dataPath, []byte(`{"add_task":{"title":"updated task"}}`), 0o600))

	var req wsapi.UpdateAdviceRequest
	server := setupServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPut, r.Method)
		require.Equal(t, "/api/v1/ticketmanager/workspace/advices/7001", r.URL.Path)
		require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
		writeResponse(t, w, &wsapi.UpdateAdviceResponse{
			BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
			Advice:       testAdvice(),
		})
	}))

	setupClient(t, server.URL)
	var stdout bytes.Buffer
	setupPrinter(t, &stdout)
	cmd := NewCommand()
	cmd.SetArgs([]string{
		"update",
		"7001",
		"--description",
		"updated",
		"--data-file",
		dataPath,
	})

	err := cmd.Execute()
	require.NoError(t, err)
	require.NotNil(t, req.Description)
	require.Equal(t, "updated", *req.Description)
	require.NotNil(t, req.Data)
	require.Equal(t, wsent.AdviceData{AddTask: &wsent.AdviceDataAddTask{Title: "updated task"}}, *req.Data)
	require.Contains(t, stdout.String(), `"id": 7001`)
}

func TestApplyIgnoreAndDeleteBuildWorkspaceAdviceRequests(t *testing.T) {
	requests := make([]string, 0, 3)
	server := setupServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests = append(requests, r.Method+" "+r.URL.Path)
		switch r.Method + " " + r.URL.Path {
		case http.MethodPost + " /api/v1/ticketmanager/workspace/advices/7001/apply":
			writeResponse(t, w, &wsapi.ApplyAdviceResponse{
				BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
				Advice:       testAdvice(),
			})
		case http.MethodPost + " /api/v1/ticketmanager/workspace/advices/7001/ignore":
			ignored := testAdvice()
			ignored.Status = wsent.AdviceStatusIgnored
			writeResponse(t, w, &wsapi.IgnoreAdviceResponse{
				BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
				Advice:       ignored,
			})
		case http.MethodDelete + " /api/v1/ticketmanager/workspace/advices/7001":
			writeResponse(t, w, &wsapi.DeleteAdviceResponse{
				BaseResponse: &baseapi.BaseResponse{ErrCode: http.StatusOK},
				Advice:       testAdvice(),
			})
		default:
			require.FailNow(t, "unexpected request", r.Method+" "+r.URL.String())
		}
	}))

	setupClient(t, server.URL)
	var stdout bytes.Buffer
	setupPrinter(t, &stdout)

	applyCmd := NewCommand()
	applyCmd.SetArgs([]string{"apply", "7001"})
	require.NoError(t, applyCmd.Execute())

	ignoreCmd := NewCommand()
	ignoreCmd.SetArgs([]string{"ignore", "7001"})
	require.NoError(t, ignoreCmd.Execute())

	deleteCmd := NewCommand()
	deleteCmd.SetArgs([]string{"delete", "7001"})
	require.NoError(t, deleteCmd.Execute())

	require.Equal(t, []string{
		http.MethodPost + " /api/v1/ticketmanager/workspace/advices/7001/apply",
		http.MethodPost + " /api/v1/ticketmanager/workspace/advices/7001/ignore",
		http.MethodDelete + " /api/v1/ticketmanager/workspace/advices/7001",
	}, requests)
	require.Contains(t, stdout.String(), `"status": "ignored"`)
}

func TestCreateRejectsInvalidJSONData(t *testing.T) {
	cmd := NewCommand()
	cmd.SetArgs([]string{
		"create",
		"--workspace-id",
		"1001",
		"--description",
		"create task",
		"--type",
		"add_task",
		"--data",
		`{`,
	})

	err := cmd.Execute()
	require.ErrorContains(t, err, "invalid JSON data")
}

func TestUpdateRequiresAtLeastOneField(t *testing.T) {
	cmd := NewCommand()
	cmd.SetArgs([]string{"update", "7001"})

	err := cmd.Execute()
	require.ErrorContains(t, err, "at least one update field is required")
}

func TestInvalidAdviceIDReturnsError(t *testing.T) {
	cmd := NewCommand()
	cmd.SetArgs([]string{"get", "invalid"})

	err := cmd.Execute()
	require.ErrorContains(t, err, `invalid advice id "invalid"`)
}

func setupServer(t *testing.T, handler http.Handler) *httptest.Server {
	t.Helper()

	server := httptest.NewServer(handler)
	t.Cleanup(server.Close)
	return server
}

func setupClient(t *testing.T, serverURL string) {
	t.Helper()

	t.Setenv(consts.EnvDevOpsAPIToken, "test-token")
	require.NoError(t, common.SetupClient(serverURL, time.Second))
}

func setupPrinter(t *testing.T, output *bytes.Buffer) {
	t.Helper()

	require.NoError(t, common.SetupPrinter("json", output))
}

func testAdvice() *wsent.Advice {
	appliedAt := int64(300)
	return &wsent.Advice{
		Updatable:   base.Updatable{Base: base.Base{ID: 7001, CreatedAt: 100}, UpdatedAt: &appliedAt},
		WorkspaceID: 1001,
		Description: "add task",
		Type:        wsent.AdivceTypeAddTask,
		Status:      wsent.AdviceStatusCreated,
		Data:        wsent.AdviceData{AddTask: &wsent.AdviceDataAddTask{Title: "task title"}},
		AppliedAt:   &appliedAt,
	}
}

func writeResponse(t *testing.T, w http.ResponseWriter, value any) {
	t.Helper()

	w.Header().Set("Content-Type", "application/json")
	require.NoError(t, json.NewEncoder(w).Encode(value))
}
