package rest_api_test

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

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

type queryFixture struct {
	objectName string
	recordID   string
	recordName string
	path       string

	batchNamesByID map[string]string
}

type queryResponse struct {
	TotalSize      int              `json:"totalSize"`
	Done           bool             `json:"done"`
	NextRecordsURL string           `json:"nextRecordsUrl"`
	Records        []map[string]any `json:"records"`
}

func createAccountFixture(t *testing.T, deleteOnCleanup bool) queryFixture {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	recordName := fmt.Sprintf("Codex Query Contract %d", time.Now().UnixNano())

	resp, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Name": recordName,
	})
	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)

	if deleteOnCleanup {
		t.Cleanup(func() {
			deleteAccountRecordIfPresent(t, body.ID)
		})
	}

	return queryFixture{
		objectName: "Account",
		recordID:   body.ID,
		recordName: recordName,
	}
}

func createAccountBatchFixture(t *testing.T, count int) queryFixture {
	t.Helper()

	require.GreaterOrEqual(t, count, 1)

	client := liveapi.NewAuthenticatedClient(t)
	namePrefix := fmt.Sprintf("Codex Query More %d", time.Now().UnixNano())
	recordIDs := make([]string, 0, count)
	recordNamesByID := make(map[string]string, count)

	const chunkSize = 200
	for start := 0; start < count; start += chunkSize {
		end := start + chunkSize
		if end > count {
			end = count
		}

		records := make([]map[string]any, 0, end-start)
		names := make([]string, 0, end-start)
		for i := start; i < end; i++ {
			recordName := fmt.Sprintf("%s %03d", namePrefix, i)
			names = append(names, recordName)
			records = append(records, map[string]any{
				"attributes": map[string]any{"type": "Account"},
				"Name":       recordName,
			})
		}

		resp, err := client.DoJSON(http.MethodPost, "/composite/sobjects", map[string]any{
			"allOrNone": false,
			"records":   records,
		})
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, 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.Len(t, body, len(names))

		for i, result := range body {
			require.True(t, result.Success)
			require.NotEmpty(t, result.ID)
			require.Empty(t, result.Errors)

			recordIDs = append(recordIDs, result.ID)
			recordNamesByID[result.ID] = names[i]
		}
	}

	t.Cleanup(func() {
		for start := len(recordIDs); start > 0; start -= chunkSize {
			begin := start - chunkSize
			if begin < 0 {
				begin = 0
			}
			deleteAccountRecordsIfPresent(t, recordIDs[begin:start])
		}
	})

	return queryFixture{
		objectName:     "Account",
		recordName:     namePrefix,
		batchNamesByID: recordNamesByID,
	}
}

func deleteAccountRecord(t *testing.T, recordID string) {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.Do(http.MethodDelete, "/sobjects/Account/"+recordID, "", nil)
	require.NoError(t, err)
	require.Equal(t, http.StatusNoContent, resp.StatusCode)
	require.NoError(t, resp.Body.Close())
}

func deleteAccountRecordIfPresent(t *testing.T, recordID string) {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.Do(http.MethodDelete, "/sobjects/Account/"+recordID, "", nil)
	if err != nil {
		t.Logf("cleanup delete failed for Account %s: %v", 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 Account %s: %s", resp.StatusCode, recordID, strings.TrimSpace(string(body)))
}

func deleteAccountRecordsIfPresent(t *testing.T, recordIDs []string) {
	t.Helper()

	if len(recordIDs) == 0 {
		return
	}

	client := liveapi.NewAuthenticatedClient(t)
	path := "/composite/sobjects?ids=" + url.QueryEscape(strings.Join(recordIDs, ",")) + "&allOrNone=false"
	resp, err := client.Do(http.MethodDelete, path, "", nil)
	if err != nil {
		t.Logf("cleanup composite delete failed for Accounts %v: %v", recordIDs, err)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusOK {
		return
	}

	body := liveapi.ReadBody(t, resp)
	t.Logf("cleanup composite delete returned status %d for Accounts %v: %s", resp.StatusCode, recordIDs, strings.TrimSpace(string(body)))
}

func assertQueryRecord(t *testing.T, record map[string]any, objectName, recordID, recordName string) {
	t.Helper()

	id, ok := record["Id"].(string)
	require.True(t, ok)
	assert.Equal(t, recordID, id)

	name, ok := record["Name"].(string)
	require.True(t, ok)
	assert.Equal(t, recordName, name)

	attributes, ok := record["attributes"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, objectName, attributes["type"])

	recordURL, ok := attributes["url"].(string)
	require.True(t, ok)
	assert.Equal(t, fmt.Sprintf("/services/data/v63.0/sobjects/%s/%s", objectName, recordID), recordURL)
}

func assertSalesforceErrorMessageContains(t *testing.T, resp *http.Response, status int, code, messageFragment string) {
	t.Helper()

	require.Equal(t, status, resp.StatusCode)
	body := liveapi.ReadBody(t, resp)

	var payload []map[string]any
	require.NoError(t, json.Unmarshal(body, &payload))
	require.NotEmpty(t, payload)

	assert.Equal(t, code, payload[0]["errorCode"])

	message, ok := payload[0]["message"].(string)
	require.True(t, ok)
	assert.Contains(t, strings.ToLower(message), strings.ToLower(messageFragment))
}
