package scenarios_test

// Scenario tests for data integrity — field labels, ListView persistence.
//
// DATA-004: Verify field describe returns Label (not just ApiName)
// DATA-005: Verify ListView modifications persist via API
// These are API-level tests for issues originally found in the UI.

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

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

// ---------- DATA-004: Field describe returns Label, not just ApiName ----------

func TestDATA004_FieldLabelsInDescribe(t *testing.T) {
	// Scenario: The PDF reported field Labels reverting to API Names after
	// data refresh. At the API level, GET /sobjects/{object}/describe should
	// return fields with both `name` (ApiName) and `label` (human-readable).
	// If `label` equals `name` for fields that should have distinct labels,
	// the UI will show API Names instead of Labels.

	client := liveapi.NewAuthenticatedClient(t)

	resp, err := client.Do(http.MethodGet, "/sobjects/Account/describe", "", nil)
	require.NoError(t, err)

	if resp.StatusCode == http.StatusNotFound {
		t.Fatal("Account object does not exist — cannot test field labels")
	}

	body := liveapi.ReadBody(t, resp)
	require.Equal(t, http.StatusOK, resp.StatusCode,
		"Account describe failed: %s", string(body))

	var describe struct {
		Name   string `json:"name"`
		Fields []struct {
			Name  string `json:"name"`
			Label string `json:"label"`
		} `json:"fields"`
	}
	require.NoError(t, json.Unmarshal(body, &describe))
	require.NotEmpty(t, describe.Fields, "Account should have fields")

	// Check that system fields have proper Labels (not just ApiName echo)
	fieldsChecked := 0
	fieldsWithDistinctLabel := 0

	for _, f := range describe.Fields {
		assert.NotEmpty(t, f.Label,
			"field %s has empty Label — UI will show nothing or ApiName", f.Name)

		if f.Label != "" {
			fieldsChecked++
			if f.Label != f.Name {
				fieldsWithDistinctLabel++
			}
		}
	}

	t.Logf("Account: %d fields checked, %d have Label != ApiName",
		fieldsChecked, fieldsWithDistinctLabel)

	// At minimum, some fields should have distinct Labels (e.g., "Created Date" vs "CreatedDate")
	// If ALL fields have Label == Name, the metadata is likely broken
	if fieldsChecked > 5 {
		assert.Greater(t, fieldsWithDistinctLabel, 0,
			"FINDING: All field Labels equal their ApiNames — "+
				"metadata may have lost Label values during import/refresh")
	}
}

// ---------- DATA-005: ListView changes persist via API ----------

func TestDATA005_ListViewPersistence(t *testing.T) {
	// Scenario: The PDF reported ListView changes not saving. This test
	// verifies at the API level: create/update a ListView, then re-read it
	// to confirm the changes persisted.

	client := liveapi.NewAuthenticatedClient(t)

	// Step 1: Check if Account listviews endpoint works
	listResp, err := client.Do(http.MethodGet, "/sobjects/Account/listviews", "", nil)
	require.NoError(t, err)

	if listResp.StatusCode == http.StatusNotFound {
		t.Fatal("Account listviews endpoint not available")
	}

	listBody := liveapi.ReadBody(t, listResp)

	if listResp.StatusCode != http.StatusOK {
		t.Fatalf("Account listviews failed (status %d): %s", listResp.StatusCode, string(listBody))
	}

	var listResult struct {
		Listviews []struct {
			ID    string `json:"id"`
			Label string `json:"label"`
		} `json:"listviews"`
	}
	require.NoError(t, json.Unmarshal(listBody, &listResult))

	if len(listResult.Listviews) == 0 {
		t.Skip("no listviews found for Account — cannot test persistence")
	}

	listViewID := listResult.Listviews[0].ID
	originalLabel := listResult.Listviews[0].Label
	t.Logf("testing with ListView %s (label=%q)", listViewID, originalLabel)

	// Step 2: Update the ListView (change label)
	newLabel := fmt.Sprintf("Test_%d", time.Now().UnixNano())
	updateResp, err := client.DoJSON(http.MethodPatch,
		"/sobjects/_System_ListView/"+listViewID,
		map[string]any{"Label": newLabel})
	require.NoError(t, err)

	updateBody := liveapi.ReadBody(t, updateResp)

	if updateResp.StatusCode != http.StatusNoContent && updateResp.StatusCode != http.StatusOK {
		t.Skipf("ListView update not supported (status %d): %s",
			updateResp.StatusCode, string(updateBody))
	}

	// Restore original label on cleanup
	t.Cleanup(func() {
		restoreResp, _ := client.DoJSON(http.MethodPatch,
			"/sobjects/_System_ListView/"+listViewID,
			map[string]any{"Label": originalLabel})
		if restoreResp != nil {
			restoreResp.Body.Close()
		}
	})

	// Step 3: Re-read the ListView and verify the change persisted
	verifyResp, err := client.Do(http.MethodGet, "/sobjects/Account/listviews", "", nil)
	require.NoError(t, err)

	verifyBody := liveapi.ReadBody(t, verifyResp)
	require.Equal(t, http.StatusOK, verifyResp.StatusCode)

	var verifyResult struct {
		Listviews []struct {
			ID    string `json:"id"`
			Label string `json:"label"`
		} `json:"listviews"`
	}
	require.NoError(t, json.Unmarshal(verifyBody, &verifyResult))

	// Find our ListView in the results
	found := false
	for _, lv := range verifyResult.Listviews {
		if lv.ID == listViewID {
			found = true
			assert.Equal(t, newLabel, lv.Label,
				"FINDING: ListView label change did not persist — "+
					"expected %q but got %q. This confirms the 'ListView not saving' issue.",
				newLabel, lv.Label)
			break
		}
	}

	assert.True(t, found, "ListView %s not found after update", listViewID)
}

// ---------- DATA-006: Describe response A/B shape comparison ----------

func TestDATA006_DescribeShapeComparison(t *testing.T) {
	// Scenario: Compare the Account describe response structure between
	// NexusCRM and the expected Salesforce shape. Key fields that must
	// exist: name, label, keyPrefix, queryable, fields[].name, fields[].label,
	// fields[].type, fields[].createable, fields[].updateable.

	client := liveapi.NewAuthenticatedClient(t)

	resp, err := client.Do(http.MethodGet, "/sobjects/Account/describe", "", nil)
	require.NoError(t, err)

	if resp.StatusCode == http.StatusNotFound {
		t.Fatal("Account object does not exist")
	}

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

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

	// Top-level keys that Salesforce returns
	requiredTopKeys := []string{"name", "label", "queryable", "fields", "urls"}
	for _, key := range requiredTopKeys {
		assert.Contains(t, describe, key,
			"describe response missing required top-level key %q", key)
	}

	// Check fields array structure
	fields, ok := describe["fields"].([]any)
	require.True(t, ok, "fields should be an array")
	require.NotEmpty(t, fields, "fields should not be empty")

	firstField, ok := fields[0].(map[string]any)
	require.True(t, ok, "first field should be a map")

	requiredFieldKeys := []string{"name", "label", "type", "createable", "updateable"}
	for _, key := range requiredFieldKeys {
		assert.Contains(t, firstField, key,
			"field object missing required key %q", key)
	}

	t.Logf("Account describe: %d top-level keys, %d fields, first field has keys: %v",
		len(describe), len(fields), func() []string {
			keys := make([]string, 0, len(firstField))
			for k := range firstField {
				keys = append(keys, k)
			}
			return keys
		}())
}

// ---------- DATA-007: Validation rule with composite Salesforce formula ----------

func TestDATA007_CompositeValidationFormula(t *testing.T) {
	// Scenario: The PDF showed AllowCloseWon validation rule uses a composite
	// formula: NOT(ISCHANGED(StageName)) combined with $Profile.Name check.
	// This tests a formula closer to the real-world one, not just individual
	// functions.
	//
	// Formula: NOT(ISBLANK(Industry)) && ISBLANK(Name)
	// Meaning: If Industry is not blank AND Name is blank, reject.

	ruleName := fmt.Sprintf("Composite_%d", time.Now().UnixNano())
	ensureValidationRule(t, "Account", ruleName,
		`NOT(ISBLANK(Industry)) && ISBLANK(Name)`,
		"Cannot have Industry without Name",
	)

	client := liveapi.NewAuthenticatedClient(t)

	// Act 1: Industry set, Name blank — should be REJECTED
	resp1, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Industry": "Technology",
		// Name intentionally omitted
	})
	require.NoError(t, err)

	body1 := liveapi.ReadBody(t, resp1)

	// Clean up if record was accidentally created
	if resp1.StatusCode == http.StatusCreated {
		var c struct{ ID string `json:"id"` }
		if json.Unmarshal(body1, &c) == nil && c.ID != "" {
			t.Cleanup(func() {
				dr, _ := client.Do(http.MethodDelete, "/sobjects/Account/"+c.ID, "", nil)
				if dr != nil {
					dr.Body.Close()
				}
			})
		}
	}

	assert.Equal(t, http.StatusBadRequest, resp1.StatusCode,
		"composite formula NOT(ISBLANK(Industry)) && ISBLANK(Name) should reject: %s",
		string(body1))

	// Act 2: Both Industry and Name set — should be ALLOWED
	resp2, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Name":     fmt.Sprintf("Composite_%d", time.Now().UnixNano()),
		"Industry": "Technology",
	})
	require.NoError(t, err)

	body2 := liveapi.ReadBody(t, resp2)

	if resp2.StatusCode == http.StatusCreated {
		var c struct{ ID string `json:"id"` }
		if json.Unmarshal(body2, &c) == nil && c.ID != "" {
			t.Cleanup(func() {
				dr, _ := client.Do(http.MethodDelete, "/sobjects/Account/"+c.ID, "", nil)
				if dr != nil {
					dr.Body.Close()
				}
			})
		}
	}

	// 201 means allowed (correct), 400 means another rule blocked it
	assert.NotEqual(t, http.StatusInternalServerError, resp2.StatusCode,
		"composite formula should not cause 500: %s", string(body2))
}

// ---------- DATA-008: Flow import preserves boolean assignment values ----------

func TestDATA008_FlowImportBooleanPreservation(t *testing.T) {
	// Scenario: The PDF issue is about IMPORTED flows showing {null} for
	// boolean values. FLOW-001 tests a flow created via API. This test
	// specifically checks that a flow created with $GlobalConstant.True
	// in elementReference format preserves the value when read back.
	//
	// The bug: backend stores the assignment value but doesn't resolve
	// $GlobalConstant.True, so the frontend sees raw {elementReference: ...}
	// and renders it as {null}.

	client := liveapi.NewAuthenticatedClient(t)
	devName := fmt.Sprintf("ImportBool%d", time.Now().UnixNano())

	// Create a flow with boolean assignment using elementReference
	flowPayload := map[string]any{
		"FullName":    devName,
		"Status":      "Draft",
		"ProcessType": "AutoLaunchedFlow",
		"Metadata": map[string]any{
			"label":      devName,
			"apiVersion": "63.0",
			"status":     "Draft",
			"start": map[string]any{
				"connector": map[string]any{
					"targetReference": "SetBool",
				},
			},
			"variables": []map[string]any{{
				"name":     "MyBool",
				"dataType": "Boolean",
				"isOutput": true,
			}},
			"assignments": []map[string]any{{
				"name": "SetBool",
				"assignmentItems": []map[string]any{{
					"assignToReference": "MyBool",
					"operator":          "Assign",
					"value": map[string]any{
						"elementReference": "$GlobalConstant.True",
					},
				}},
			}},
		},
	}

	createResp, err := client.DoJSON(http.MethodPost, "/tooling/sobjects/Flow", flowPayload)
	require.NoError(t, err)

	if createResp.StatusCode != http.StatusCreated {
		body := liveapi.ReadBody(t, createResp)
		t.Fatalf("could not create flow (status %d): %s", createResp.StatusCode, string(body))
	}

	var createBody struct {
		ID string `json:"id"`
	}
	liveapi.DecodeJSON(t, createResp, &createBody)
	require.NotEmpty(t, createBody.ID)

	flowID := createBody.ID
	t.Cleanup(func() {
		dr, _ := client.Do(http.MethodDelete, "/tooling/sobjects/Flow/"+flowID, "", nil)
		if dr != nil {
			dr.Body.Close()
		}
	})

	// Read the flow back via Tooling API
	getResp, err := client.Do(http.MethodGet, "/tooling/sobjects/Flow/"+flowID, "", nil)
	require.NoError(t, err)

	getBody := liveapi.ReadBody(t, getResp)
	require.Equal(t, http.StatusOK, getResp.StatusCode,
		"GET flow failed: %s", string(getBody))

	var flowData map[string]any
	require.NoError(t, json.Unmarshal(getBody, &flowData))

	// Navigate to the assignment value in the metadata
	metadata, _ := flowData["Metadata"].(map[string]any)
	if metadata == nil {
		// Try parsing Metadata as string (it might be stored as JSON string)
		if metaStr, ok := flowData["Metadata"].(string); ok {
			require.NoError(t, json.Unmarshal([]byte(metaStr), &metadata))
		}
	}

	require.NotNil(t, metadata, "flow Metadata should be present")

	assignments, _ := metadata["assignments"].([]any)
	require.NotEmpty(t, assignments, "flow should have assignments")

	firstAssignment, _ := assignments[0].(map[string]any)
	require.NotNil(t, firstAssignment)

	items, _ := firstAssignment["assignmentItems"].([]any)
	require.NotEmpty(t, items, "assignment should have items")

	firstItem, _ := items[0].(map[string]any)
	require.NotNil(t, firstItem)

	value := firstItem["value"]
	t.Logf("stored assignment value: %#v", value)

	// The value should NOT be null — it should preserve the $GlobalConstant.True reference
	assert.NotNil(t, value,
		"FINDING: assignment value is null after save — "+
			"$GlobalConstant.True was lost during storage. "+
			"This is the root cause of Flow Builder showing {null}.")

	// Check the structure
	if valueMap, ok := value.(map[string]any); ok {
		ref, _ := valueMap["elementReference"].(string)
		assert.Equal(t, "$GlobalConstant.True", ref,
			"assignment should preserve $GlobalConstant.True reference")
	}
}

// ---------- Helpers ----------

func querySOQL(t testing.TB, client *liveapi.Client, soql string) []json.RawMessage {
	t.Helper()
	resp, err := client.Do(http.MethodGet, "/query?q="+url.QueryEscape(soql), "", nil)
	require.NoError(t, err)
	body := liveapi.ReadBody(t, resp)
	require.Equal(t, http.StatusOK, resp.StatusCode, "SOQL failed: %s", string(body))

	var result struct {
		Records []json.RawMessage `json:"records"`
	}
	require.NoError(t, json.Unmarshal(body, &result))
	return result.Records
}
