package arrange

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/tests/contract/internal/liveapi"
	"github.com/stretchr/testify/require"
)

type FlowFixture struct {
	VersionID     string
	DeveloperName string
	Label         string
}

func UniqueName(prefix string) string {
	return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}

func FetchUserID(t testing.TB) string {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodGet, "/services/oauth2/userinfo", nil)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		UserID string `json:"user_id"`
		Sub    string `json:"sub"`
	}
	liveapi.DecodeJSON(t, resp, &body)
	if body.UserID != "" {
		return body.UserID
	}
	return body.Sub
}

func CreateSObject(t testing.TB, objectName string, fields map[string]any) string {
	t.Helper()

	payload := make(map[string]any)
	for k, v := range fields {
		payload[k] = v
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/"+objectName, payload)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	var body struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, resp, &body)

	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)
	require.Empty(t, body.Errors)
	return body.ID
}

func DeleteSObjectIfPresent(t testing.TB, objectName, recordID string) {
	t.Helper()

	if strings.TrimSpace(recordID) == "" {
		return
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.Do(http.MethodDelete, "/sobjects/"+objectName+"/"+recordID, "", nil)
	if err != nil {
		t.Logf("cleanup delete failed for %s %s: %v", objectName, recordID, err)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusNotFound {
		return
	}
	body := liveapi.ReadBody(t, resp)
	t.Logf("cleanup delete returned status %d for %s %s: %s", resp.StatusCode, objectName, recordID, strings.TrimSpace(string(body)))
}

func CreateQuickAction(t testing.TB, objectName, actionName, label string) string {
	t.Helper()

	payload := map[string]any{
		"objectApiName": objectName,
		"name":          actionName,
		"label":         label,
		"type":          "CreateRecord",
		"icon":          "/img/icon.svg",
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/tooling/sobjects/QuickActionDefinition", payload)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	var body struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, resp, &body)

	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)
	require.Empty(t, body.Errors)
	return body.ID
}

func CreateInvocableFlow(t testing.TB, label string) FlowFixture {
	t.Helper()

	developerName := sanitizeDeveloperName(label)

	metadata := map[string]any{
		"label":      label,
		"apiVersion": "63.0",
		"status":     "Draft",
		"start": map[string]any{
			"connector": map[string]any{
				"targetReference": "AssignVar",
			},
		},
		"variables": []map[string]any{
			{
				"name":     "InputText",
				"dataType": "String",
				"isInput":  true,
			},
			{
				"name":     "OutputText",
				"dataType": "String",
				"isOutput": true,
			},
		},
		"assignments": []map[string]any{
			{
				"name": "AssignVar",
				"assignmentItems": []map[string]any{
					{
						"assignToReference": "OutputText",
						"operator":          "Assign",
						"value":             "InputText",
					},
				},
			},
		},
	}

	payload := map[string]any{
		"Status":      "Draft",
		"ProcessType": "AutoLaunchedFlow",
		"Metadata":    metadata,
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/tooling/sobjects/Flow", payload)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	var body struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, resp, &body)

	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)
	require.Empty(t, body.Errors)

	activateResp, err := client.DoJSON(http.MethodPatch, "/tooling/sobjects/Flow/"+body.ID, map[string]any{
		"Status": "Active",
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusNoContent, activateResp.StatusCode)
	require.NoError(t, activateResp.Body.Close())

	return FlowFixture{
		VersionID:     body.ID,
		DeveloperName: developerName,
		Label:         label,
	}
}

func CreatePausedFlowInterview(t testing.TB, flowVersionID, currentElement string) string {
	t.Helper()

	payload := map[string]any{
		"FlowVersionId":   flowVersionID,
		"InterviewLabel":  "Codex Flow Interview",
		"InterviewStatus": "Paused",
		"CurrentElement":  currentElement,
		"ContextData":     `{"InputText":"Resume"}`,
	}

	if userID := FetchUserID(t); userID != "" {
		payload["OwnerId"] = userID
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/FlowInterview", payload)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	var body struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, resp, &body)

	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)
	require.Empty(t, body.Errors)
	return body.ID
}

func CreateProcessDefinition(t testing.TB, objectName, name string) string {
	t.Helper()

	metadataXML := fmt.Sprintf(
		`<ApprovalProcess><label>%s</label><active>true</active><approvalStep><name>Step1</name><label>Step 1</label></approvalStep></ApprovalProcess>`,
		name,
	)

	payload := map[string]any{
		"Name":          name,
		"DeveloperName": sanitizeDeveloperName(name),
		"TableEnumOrId": objectName,
		"State":         "Active",
		"Metadata":      metadataXML,
	}

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/ProcessDefinition", payload)
	require.NoError(t, err)

	if resp.StatusCode != http.StatusCreated {
		cfg := liveapi.LoadConfig(t)
		if cfg.TargetName == "sandbox" {
			body := liveapi.ReadBody(t, resp)
			t.Skipf("ProcessDefinition not creatable in sandbox: status %d body %s", resp.StatusCode, strings.TrimSpace(string(body)))
		}
		require.Equal(t, http.StatusCreated, resp.StatusCode)
	}

	var body struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, resp, &body)

	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)
	require.Empty(t, body.Errors)
	return body.ID
}

func sanitizeDeveloperName(label string) string {
	var sb strings.Builder
	for _, r := range label {
		switch {
		case r >= 'a' && r <= 'z':
			sb.WriteRune(r)
		case r >= 'A' && r <= 'Z':
			sb.WriteRune(r)
		case r >= '0' && r <= '9':
			sb.WriteRune(r)
		case r == ' ' || r == '-':
			sb.WriteRune('_')
		}
	}
	if sb.Len() == 0 {
		return "Flow"
	}
	return sb.String()
}

func DecodeJSONBody(t testing.TB, resp *http.Response, out any) {
	t.Helper()

	defer resp.Body.Close()
	require.NoError(t, json.NewDecoder(resp.Body).Decode(out))
}
