package rest_api_test

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

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

type searchFixture struct {
	path             string
	term             string
	accountID        string
	accountName      string
	contactID        string
	contactFirstName string
	contactLastName  string
}

type searchCallResult struct {
	StatusCode int
	Raw        []byte
	Body       searchResponse
}

type searchResponse struct {
	SearchRecords []map[string]any `json:"searchRecords"`
}

func createSearchFixture(t *testing.T) searchFixture {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	term := fmt.Sprintf("CodexSearch%d", time.Now().UnixNano())
	accountName := "Account " + term
	contactFirstName := "Codex"
	contactLastName := term

	accountResp, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Name": accountName,
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, accountResp.StatusCode)

	var accountBody struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, accountResp, &accountBody)
	require.True(t, accountBody.Success)
	require.NotEmpty(t, accountBody.ID)
	require.Empty(t, accountBody.Errors)

	contactResp, err := client.DoJSON(http.MethodPost, "/sobjects/Contact", map[string]any{
		"FirstName": contactFirstName,
		"LastName":  contactLastName,
		"AccountId": accountBody.ID,
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, contactResp.StatusCode)

	var contactBody struct {
		ID      string           `json:"id"`
		Success bool             `json:"success"`
		Errors  []map[string]any `json:"errors"`
	}
	liveapi.DecodeJSON(t, contactResp, &contactBody)
	require.True(t, contactBody.Success)
	require.NotEmpty(t, contactBody.ID)
	require.Empty(t, contactBody.Errors)

	t.Cleanup(func() {
		deleteContactRecordIfPresent(t, contactBody.ID)
		deleteAccountRecordIfPresent(t, accountBody.ID)
	})

	return searchFixture{
		term:             term,
		accountID:        accountBody.ID,
		accountName:      accountName,
		contactID:        contactBody.ID,
		contactFirstName: contactFirstName,
		contactLastName:  contactLastName,
	}
}

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

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

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 doAuthenticatedSearchRequest(t *testing.T, path string) searchCallResult {
	t.Helper()

	return doAuthenticatedSearchJSONRequest(t, http.MethodGet, path, nil)
}

func doAuthenticatedSearchJSONRequest(t *testing.T, method, path string, body any) searchCallResult {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(method, path, body)
	require.NoError(t, err)
	return readSearchCallResult(t, resp)
}

func executeSearchEventually(t *testing.T, path string, predicate func(body searchResponse) bool) searchCallResult {
	t.Helper()

	return executeSearchJSONEventually(t, http.MethodGet, path, nil, predicate)
}

func executeSearchJSONEventually(t *testing.T, method, path string, body any, predicate func(body searchResponse) bool) searchCallResult {
	t.Helper()

	deadline := time.Now().Add(15 * time.Second)
	var last searchCallResult

	for {
		last = doAuthenticatedSearchJSONRequest(t, method, path, body)
		if last.StatusCode == http.StatusOK && predicate(last.Body) {
			return last
		}
		if time.Now().After(deadline) {
			return last
		}
		time.Sleep(time.Second)
	}
}

func readSearchCallResult(t *testing.T, resp *http.Response) searchCallResult {
	t.Helper()

	raw := liveapi.ReadBody(t, resp)
	result := searchCallResult{
		StatusCode: resp.StatusCode,
		Raw:        raw,
	}
	if resp.StatusCode == http.StatusOK {
		require.NoError(t, json.Unmarshal(raw, &result.Body))
	}
	return result
}

func hasSearchRecord(body searchResponse, objectName, recordID string) bool {
	for _, record := range body.SearchRecords {
		id, ok := record["Id"].(string)
		if !ok || id != recordID {
			continue
		}

		attributes, ok := record["attributes"].(map[string]any)
		if !ok {
			continue
		}
		recordType, ok := attributes["type"].(string)
		if ok && recordType == objectName {
			return true
		}
	}
	return false
}

func findSearchRecord(t *testing.T, body searchResponse, objectName, recordID string) map[string]any {
	t.Helper()

	for _, record := range body.SearchRecords {
		id, ok := record["Id"].(string)
		if !ok || id != recordID {
			continue
		}

		attributes, ok := record["attributes"].(map[string]any)
		if !ok {
			continue
		}

		recordType, ok := attributes["type"].(string)
		if ok && recordType == objectName {
			return record
		}
	}

	t.Fatalf("search response missing %s record %s", objectName, recordID)
	return nil
}

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

	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 assertSearchErrorMessageContains(t *testing.T, result searchCallResult, status int, messageFragment string) {
	t.Helper()

	require.Equal(t, status, result.StatusCode)
	assertSearchErrorCodeAndMessageContains(t, result, "", messageFragment)
}

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

	assertSearchErrorMessageContains(t, readSearchCallResult(t, resp), status, messageFragment)
}

func assertSearchErrorCodeAndMessageContains(t *testing.T, result searchCallResult, code, messageFragment string) {
	t.Helper()

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

	if code != "" {
		assert.Equal(t, code, payload[0]["errorCode"])
	} else {
		assert.NotEmpty(t, payload[0]["errorCode"])
	}

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