package rest_api_test

// Docs:
// - Endpoint: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
// - Request: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
// - Response: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
// - Sub-schema: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
// - Errors: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm

import (
	"net/http"
	"strings"
	"testing"

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

func TestPostComposite(t *testing.T) {
	contracttest.EnsureLocalRESTBaseline(t)

	type compositeResult struct {
		HttpStatusCode int         `json:"httpStatusCode"`
		ReferenceId    string      `json:"referenceId"`
		Body           interface{} `json:"body"`
	}
	type compositeResponse struct {
		CompositeResponse []compositeResult `json:"compositeResponse"`
	}

	testCases := []struct {
		name    string
		arrange func(t *testing.T) map[string]any
		act     func(t *testing.T, payload map[string]any) *http.Response
		assert  func(t *testing.T, resp *http.Response)
	}{
		{
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
			name: "supports reference chaining across subrequests",
			arrange: func(t *testing.T) map[string]any {
				accountName := arrange.UniqueName("Composite Account")
				return map[string]any{
					"allOrNone": true,
					"compositeRequest": []map[string]any{
						{
							"method":      http.MethodPost,
							"url":         "/services/data/v63.0/sobjects/Account",
							"referenceId": "NewAccount",
							"body": map[string]any{
								"Name": accountName,
							},
						},
						{
							"method":      http.MethodGet,
							"url":         "/services/data/v63.0/sobjects/Account/@{NewAccount.id}",
							"referenceId": "FetchAccount",
						},
					},
				}
			},
			act: func(t *testing.T, payload map[string]any) *http.Response {
				client := liveapi.NewAuthenticatedClient(t)
				resp, err := client.DoJSON(http.MethodPost, "/composite", payload)
				require.NoError(t, err)
				return resp
			},
			assert: func(t *testing.T, resp *http.Response) {
				require.Equal(t, http.StatusOK, resp.StatusCode)
				contracttest.AssertJSONContentType(t, resp)

				var body compositeResponse
				liveapi.DecodeJSON(t, resp, &body)
				require.Len(t, body.CompositeResponse, 2)

				createResult := body.CompositeResponse[0]
				assert.Equal(t, "NewAccount", createResult.ReferenceId)
				assert.Equal(t, http.StatusCreated, createResult.HttpStatusCode)
				createBody, ok := createResult.Body.(map[string]any)
				require.True(t, ok)
				success, ok := createBody["success"].(bool)
				require.True(t, ok)
				assert.True(t, success)
				errors, ok := createBody["errors"].([]any)
				require.True(t, ok)
				assert.Empty(t, errors)
				recordID, ok := createBody["id"].(string)
				require.True(t, ok)
				contracttest.AssertSalesforceID(t, recordID)
				t.Cleanup(func() { arrange.DeleteSObjectIfPresent(t, "Account", recordID) })

				fetchResult := body.CompositeResponse[1]
				assert.Equal(t, "FetchAccount", fetchResult.ReferenceId)
				assert.Equal(t, http.StatusOK, fetchResult.HttpStatusCode)
				fetchBody, ok := fetchResult.Body.(map[string]any)
				require.True(t, ok)
				assert.Equal(t, recordID, fetchBody["Id"])
				attributes, ok := fetchBody["attributes"].(map[string]any)
				require.True(t, ok)
				assert.Equal(t, "Account", attributes["type"])
				assert.Contains(t, attributes["url"], recordID)
			},
		},
		{
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
			name: "accepts the trailing slash endpoint variant when available",
			arrange: func(t *testing.T) map[string]any {
				return map[string]any{
					"allOrNone": false,
					"compositeRequest": []map[string]any{
						{
							"method":      http.MethodGet,
							"url":         "/services/data/v63.0/limits",
							"referenceId": "Limits",
						},
					},
				}
			},
			act: func(t *testing.T, payload map[string]any) *http.Response {
				client := liveapi.NewAuthenticatedClient(t)
				resp, err := client.DoJSON(http.MethodPost, "/composite/", payload)
				require.NoError(t, err)
				return resp
			},
			assert: func(t *testing.T, resp *http.Response) {
				switch resp.StatusCode {
				case http.StatusOK:
					contracttest.AssertJSONContentType(t, resp)

					var body compositeResponse
					liveapi.DecodeJSON(t, resp, &body)
					require.Len(t, body.CompositeResponse, 1)
					assert.Equal(t, http.StatusOK, body.CompositeResponse[0].HttpStatusCode)
				case http.StatusNotFound, http.StatusForbidden:
					body := liveapi.ReadBody(t, resp)
					t.Skipf("composite trailing slash variant unavailable (status %d body %s)", resp.StatusCode, strings.TrimSpace(string(body)))
				default:
					body := liveapi.ReadBody(t, resp)
					t.Fatalf("unexpected status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
				}
			},
		},
		{
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.254.0.api_rest.meta/api_rest/resources_composite_composite.htm
			name: "rejects composite subrequests targeting composite endpoints",
			arrange: func(t *testing.T) map[string]any {
				return map[string]any{
					"allOrNone": false,
					"compositeRequest": []map[string]any{
						{
							"method":      http.MethodPost,
							"url":         "/services/data/v63.0/composite",
							"referenceId": "NestedComposite",
							"body": map[string]any{
								"compositeRequest": []map[string]any{},
							},
						},
					},
				}
			},
			act: func(t *testing.T, payload map[string]any) *http.Response {
				client := liveapi.NewAuthenticatedClient(t)
				resp, err := client.DoJSON(http.MethodPost, "/composite", payload)
				require.NoError(t, err)
				return resp
			},
			assert: func(t *testing.T, resp *http.Response) {
				require.Equal(t, http.StatusOK, resp.StatusCode)
				contracttest.AssertJSONContentType(t, resp)

				var body compositeResponse
				liveapi.DecodeJSON(t, resp, &body)
				require.Len(t, body.CompositeResponse, 1)

				result := body.CompositeResponse[0]
				assert.Equal(t, "NestedComposite", result.ReferenceId)
				assert.Equal(t, http.StatusBadRequest, result.HttpStatusCode)
				errArray, ok := result.Body.([]interface{})
				require.True(t, ok)
				require.NotEmpty(t, errArray)
				first, ok := errArray[0].(map[string]any)
				require.True(t, ok)
				assert.Equal(t, "INVALID_REQUEST", first["errorCode"])
				assert.NotEmpty(t, first["message"])
			},
		},
		{
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
			name: "missing bearer token is rejected",
			arrange: func(t *testing.T) map[string]any {
				return map[string]any{
					"allOrNone": true,
					"compositeRequest": []map[string]any{
						{
							"method":      http.MethodGet,
							"url":         "/services/data/v63.0/limits",
							"referenceId": "Limits",
						},
					},
				}
			},
			act: func(t *testing.T, payload map[string]any) *http.Response {
				return contracttest.DoAnonymousRequest(t, http.MethodPost, "/services/data/v63.0/composite", payload)
			},
			assert: func(t *testing.T, resp *http.Response) {
				contracttest.AssertSalesforceError(t, resp, http.StatusUnauthorized, "INVALID_SESSION_ID")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			payload := tc.arrange(t)
			resp := tc.act(t, payload)
			tc.assert(t, resp)
		})
	}
}
