package scenarios_test

// Scenario tests for validation rule execution.
//
// These verify that the formula engine correctly evaluates Salesforce-style
// validation rules containing NOT(), ISBLANK(), ISCHANGED(), and context
// variables ($Profile.Name, $UserRole.Name).

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

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

// ensureValidationRule creates a validation rule on the given object via
// Tooling API. Returns the rule ID. Skips if the endpoint is unavailable.
func ensureValidationRule(t testing.TB, objectName, ruleName, formula, errorMessage string) string {
	t.Helper()

	// Bootstrap: ensure the target object exists
	ensureObject(t, objectName, objectName, objectName+"s")

	client := liveapi.NewAuthenticatedClient(t)

	// Try NexusCRM native format first (objectApiName, name, condition)
	payload := map[string]any{
		"objectApiName": objectName,
		"name":          ruleName,
		"active":        true,
		"condition":     formula,
		"errorMessage":  errorMessage,
	}

	resp, err := client.DoJSON(http.MethodPost, "/tooling/sobjects/ValidationRule", payload)
	require.NoError(t, err)

	if resp.StatusCode == http.StatusNotFound {
		t.Fatalf("Tooling API ValidationRule endpoint returned 404 — route not implemented")
	}

	respBody := liveapi.ReadBody(t, resp)

	if resp.StatusCode == http.StatusInternalServerError {
		t.Fatalf("validation rule creation returned 500 (server error): %s", string(respBody))
	}

	require.Equal(t, http.StatusCreated, resp.StatusCode,
		"validation rule creation failed (status %d): %s", resp.StatusCode, string(respBody))

	var body struct {
		ID      string `json:"id"`
		Success bool   `json:"success"`
	}
	require.NoError(t, json.Unmarshal(respBody, &body))

	require.True(t, body.Success,
		"validation rule creation returned success=false: %s", string(respBody))
	require.NotEmpty(t, body.ID,
		"validation rule creation returned empty ID: %s", string(respBody))

	t.Cleanup(func() {
		delResp, delErr := client.Do(http.MethodDelete, "/tooling/sobjects/ValidationRule/"+body.ID, "", nil)
		if delErr == nil {
			delResp.Body.Close()
		}
	})

	return body.ID
}

// createRecord is a helper to POST a new record and return its ID.
func createRecord(t testing.TB, objectName string, fields map[string]any) string {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/"+objectName, fields)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode,
		"POST /sobjects/%s expected 201", objectName)

	var body struct {
		ID      string `json:"id"`
		Success bool   `json:"success"`
	}
	liveapi.DecodeJSON(t, resp, &body)
	require.True(t, body.Success)
	require.NotEmpty(t, body.ID)

	t.Cleanup(func() {
		delResp, delErr := client.Do(http.MethodDelete, "/sobjects/"+objectName+"/"+body.ID, "", nil)
		if delErr == nil {
			delResp.Body.Close()
		}
	})

	return body.ID
}

// updateRecord PATCHes a record and returns the response for assertion.
func updateRecord(t testing.TB, objectName, id string, fields map[string]any) *http.Response {
	t.Helper()

	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPatch, "/sobjects/"+objectName+"/"+id, fields)
	require.NoError(t, err)
	return resp
}

// ---------- VAL-001: NOT() function ----------

func TestVAL001_NotFunction(t *testing.T) {
	// Scenario: A validation rule with NOT(ISBLANK(Industry)) should reject
	// records where Industry is blank.
	//
	// Previously failed with: "unknown name NOT (1:6)"

	ruleName := fmt.Sprintf("TestNOT_%d", time.Now().UnixNano())
	ensureValidationRule(t, "Account", ruleName,
		`NOT(ISBLANK(Industry))`,
		"Industry must be blank for this test rule",
	)

	// The rule says NOT(ISBLANK(Industry)) -> error. This means:
	// if Industry is NOT blank, the rule formula evaluates to true -> error fires.
	// If Industry IS blank, formula evaluates to false -> no error.

	// Act: create Account WITH Industry (should trigger the rule)
	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Name":     fmt.Sprintf("VAL001-WithIndustry-%d", time.Now().UnixNano()),
		"Industry": "Technology",
	})
	require.NoError(t, err)

	body := liveapi.ReadBody(t, resp)
	bodyStr := string(body)

	// If the backend returned 201 (rule didn't fire), clean up the leaked record.
	if resp.StatusCode == http.StatusCreated {
		var created struct {
			ID string `json:"id"`
		}
		if json.Unmarshal(body, &created) == nil && created.ID != "" {
			t.Cleanup(func() {
				delResp, _ := client.Do(http.MethodDelete, "/sobjects/Account/"+created.ID, "", nil)
				if delResp != nil {
					delResp.Body.Close()
				}
			})
		}
	}

	// Assert: should be rejected by validation rule (400), NOT internal error (500)
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode,
		"validation rule with NOT() should return 400, not 500")

	// Must NOT contain "unknown name NOT" (the old error)
	assert.NotContains(t, bodyStr, "unknown name NOT",
		"formula engine should recognize NOT() function")

	// Should contain the rule's error message
	assert.Contains(t, bodyStr, "Industry must be blank",
		"response should contain the validation rule error message")
}

// ---------- VAL-002: ISCHANGED() function ----------

func TestVAL002_IsChangedFunction(t *testing.T) {
	// Scenario: A validation rule with ISCHANGED(Industry) should fire
	// when Industry is modified on update.

	ruleName := fmt.Sprintf("TestISCHANGED_%d", time.Now().UnixNano())
	ensureValidationRule(t, "Account", ruleName,
		`ISCHANGED(Industry)`,
		"Industry cannot be changed",
	)

	// Arrange: create an Account with Industry = "Technology"
	accountID := createRecord(t, "Account", map[string]any{
		"Name":     fmt.Sprintf("VAL002-%d", time.Now().UnixNano()),
		"Industry": "Technology",
	})

	// Act: update Industry to a different value
	resp := updateRecord(t, "Account", accountID, map[string]any{
		"Industry": "Finance",
	})

	// Assert: should be rejected (400) because ISCHANGED(Industry) is true
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode,
		"ISCHANGED(Industry) should fire when value changes")

	body := liveapi.ReadBody(t, resp)
	assert.Contains(t, string(body), "Industry cannot be changed")
}

// ---------- VAL-003: Validation runs on single Insert ----------

func TestVAL003_InsertTriggersValidation(t *testing.T) {
	// Scenario: Validation rules must execute on single-record Insert
	// (POST /sobjects/{object}), not just on Update/BulkInsert.
	//
	// Use ISBLANK(Name) as a simple rule that should block records with no Name.

	ruleName := fmt.Sprintf("TestInsert_%d", time.Now().UnixNano())
	ensureValidationRule(t, "Account", ruleName,
		`ISBLANK(Name)`,
		"Name is required by validation rule",
	)

	// Act: create Account without Name
	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Industry": "Technology",
		// Name intentionally omitted
	})
	require.NoError(t, err)

	// Assert: should be rejected by validation rule
	// Accept either 400 (validation error) or 201 + cleanup.
	// If 201, the test fails — validation rule didn't fire on Insert.
	if resp.StatusCode == http.StatusCreated {
		var body struct {
			ID string `json:"id"`
		}
		liveapi.DecodeJSON(t, resp, &body)
		if body.ID != "" {
			// Cleanup the accidentally created record
			delResp, _ := client.Do(http.MethodDelete, "/sobjects/Account/"+body.ID, "", nil)
			if delResp != nil {
				delResp.Body.Close()
			}
		}
		t.Fatal("validation rule did not fire on single-record Insert — expected 400, got 201")
	}

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode,
		"validation rule should fire on Insert, not just Update")

	// Verify the response contains the validation rule's error message,
	// not just a generic schema constraint. This distinguishes the custom
	// validation rule from a database NOT NULL constraint.
	body := liveapi.ReadBody(t, resp)
	assert.Contains(t, string(body), "Name is required by validation rule",
		"response should contain the validation rule error message, not a generic constraint")
}

// ---------- VAL-005: $Profile.Name / $UserRole.Name context variables ----------

func TestVAL005_ProfileContextVariables(t *testing.T) {
	// Scenario: A validation rule referencing $Profile.Name should evaluate
	// correctly. The PDF showed AllowCloseWon uses $Profile.Name and
	// $UserRole.Name which the formula engine could not resolve.

	// Create a rule that uses $Profile.Name (should always be non-blank for authenticated users)
	ruleName := fmt.Sprintf("TestProfile_%d", time.Now().UnixNano())
	ensureValidationRule(t, "Account", ruleName,
		`$Profile.Name = "NonExistentProfile"`,
		"Profile name check failed",
	)

	// Act: create an Account (the rule should evaluate $Profile.Name against
	// "NonExistentProfile" — since our profile is NOT "NonExistentProfile",
	// the condition is false, and the record should be ALLOWED through)
	client := liveapi.NewAuthenticatedClient(t)
	resp, err := client.DoJSON(http.MethodPost, "/sobjects/Account", map[string]any{
		"Name": fmt.Sprintf("VAL005-%d", time.Now().UnixNano()),
	})
	require.NoError(t, err)

	body := liveapi.ReadBody(t, resp)

	// The key check: did the formula engine resolve $Profile.Name WITHOUT error?
	// If the engine can't resolve it, we'll see a 500 or "unknown name" error.
	// If it resolves correctly, the condition is false (our profile != NonExistentProfile),
	// so the record should be created (201).

	if resp.StatusCode == http.StatusInternalServerError {
		t.Fatalf("formula engine cannot resolve $Profile.Name: %s", string(body))
	}

	assert.NotContains(t, string(body), "unknown name",
		"$Profile.Name should be recognized by the formula engine")
	assert.NotContains(t, string(body), "failed to evaluate",
		"validation rule should evaluate without internal error")

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