package scenarios_test

// Scenario tests for Approval Process deployment and approver resolution.
//
// APPR-001: Verifies that when an approval process is deployed with approver
//           metadata, the backend parses the approver XML and stores the
//           resolved User ID on ProcessNode (not the raw username string).

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

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

// ---------- APPR-001: Approver resolution during deploy ----------

func TestAPPR001_ApproverResolution(t *testing.T) {
	// Scenario: Deploy an approval process with approver username in XML.
	// Verify that the backend:
	// 1. Creates ProcessNode records from the approval step metadata
	// 2. Resolves the approver username to a NexusCRM User ID
	// 3. Stores ApproverUserId on the ProcessNode
	//
	// If the backend does NOT auto-create ProcessNodes from metadata,
	// that itself is a finding (approver data is lost during deploy).

	client := liveapi.NewAuthenticatedClient(t)

	// Step 1: Get the current user's info for approver reference.
	userinfoResp, err := client.DoJSON(http.MethodGet, "/services/oauth2/userinfo", nil)
	require.NoError(t, err)

	var userinfo struct {
		UserID   string `json:"user_id"`
		Sub      string `json:"sub"`
		Username string `json:"preferred_username"`
		Email    string `json:"email"`
	}
	liveapi.DecodeJSON(t, userinfoResp, &userinfo)

	approverUsername := userinfo.Username
	if approverUsername == "" {
		approverUsername = userinfo.Email
	}
	require.NotEmpty(t, approverUsername,
		"cannot determine current user's username for approver test")

	userID := userinfo.UserID
	if userID == "" {
		userID = userinfo.Sub
	}

	// Step 2: Deploy a ProcessDefinition with approval step XML containing
	// an approver username.
	uniqueName := fmt.Sprintf("TestAppr%d", time.Now().UnixNano())
	deployXML := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<ApprovalProcess>
  <label>%s</label>
  <active>true</active>
  <approvalStep>
    <name>Step1</name>
    <label>Manager Approval</label>
    <assignedApprover>
      <approver>
        <name>%s</name>
        <type>user</type>
      </approver>
    </assignedApprover>
  </approvalStep>
</ApprovalProcess>`, uniqueName, approverUsername)

	deployPayload := map[string]any{
		"Name":          uniqueName,
		"DeveloperName": uniqueName,
		"TableEnumOrId": "Account",
		"State":         "Active",
		"Metadata":      deployXML,
	}

	deployResp, err := client.DoJSON(http.MethodPost, "/sobjects/ProcessDefinition", deployPayload)
	require.NoError(t, err)

	if deployResp.StatusCode == http.StatusNotFound {
		t.Fatal("ProcessDefinition endpoint returned 404 — route not implemented")
	}

	deployBody := liveapi.ReadBody(t, deployResp)
	require.Equal(t, http.StatusCreated, deployResp.StatusCode,
		"ProcessDefinition creation failed (status %d): %s", deployResp.StatusCode, string(deployBody))

	var createResult struct {
		ID      string `json:"id"`
		Success bool   `json:"success"`
	}
	require.NoError(t, json.Unmarshal(deployBody, &createResult))
	require.True(t, createResult.Success, "ProcessDefinition creation not successful")
	require.NotEmpty(t, createResult.ID)

	processDefID := createResult.ID
	t.Logf("created ProcessDefinition %s", processDefID)

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

	// Step 3: Check if the backend auto-created ProcessNode from the XML.
	// This is the CRITICAL check — if no ProcessNode was auto-created,
	// the approval step metadata (including approver) was lost during deploy.
	soql := fmt.Sprintf(
		"SELECT Id,Name,ProcessDefinitionId,ApproverType,ApproverUserId FROM ProcessNode WHERE ProcessDefinitionId='%s'",
		processDefID,
	)
	queryResp, err := client.Do(http.MethodGet, "/query?q="+url.QueryEscape(soql), "", nil)
	require.NoError(t, err)

	queryBody := liveapi.ReadBody(t, queryResp)

	if queryResp.StatusCode != http.StatusOK {
		// Try without ApproverType/ApproverUserId (columns may not exist yet)
		soqlBasic := fmt.Sprintf(
			"SELECT Id,Name,ProcessDefinitionId FROM ProcessNode WHERE ProcessDefinitionId='%s'",
			processDefID,
		)
		queryResp2, err2 := client.Do(http.MethodGet, "/query?q="+url.QueryEscape(soqlBasic), "", nil)
		require.NoError(t, err2)
		queryBody = liveapi.ReadBody(t, queryResp2)

		if queryResp2.StatusCode != http.StatusOK {
			t.Fatalf("SOQL query for ProcessNode failed (status %d): %s", queryResp2.StatusCode, string(queryBody))
		}

		// If basic query works but extended doesn't, ApproverType/UserId columns are missing
		var basicResult struct {
			TotalSize int               `json:"totalSize"`
			Records   []json.RawMessage `json:"records"`
		}
		require.NoError(t, json.Unmarshal(queryBody, &basicResult))

		if basicResult.TotalSize == 0 {
			t.Fatal("FINDING: Backend did NOT auto-create ProcessNode from approval XML metadata. " +
				"Approval step data (including approver assignment) is lost during deploy. " +
				"This is the root cause of the 'Approver not selectable' issue from confirmed_issue.pdf.")
		}

		t.Fatalf("FINDING: ProcessNode exists but ApproverType/ApproverUserId columns are missing. " +
			"Approver data cannot be stored on the ProcessNode record. " +
			"PR #360 (schema migration) is needed.")
	}

	// Parse the full query result (with approver columns)
	var queryResult struct {
		TotalSize int               `json:"totalSize"`
		Records   []json.RawMessage `json:"records"`
	}
	require.NoError(t, json.Unmarshal(queryBody, &queryResult))

	if queryResult.TotalSize == 0 {
		t.Fatal("FINDING: Backend did NOT auto-create ProcessNode from approval XML metadata. " +
			"Approval step data (including approver assignment) is lost during deploy.")
	}

	t.Logf("found %d ProcessNode record(s) for ProcessDefinition %s", queryResult.TotalSize, processDefID)

	// Step 4: Verify the approver was resolved on the auto-created ProcessNode.
	var node map[string]any
	require.NoError(t, json.Unmarshal(queryResult.Records[0], &node))

	approverType, _ := node["ApproverType"].(string)
	approverUserID, _ := node["ApproverUserId"].(string)

	t.Logf("ProcessNode: ApproverType=%q, ApproverUserId=%q", approverType, approverUserID)

	// The approver should be resolved — not empty, not the raw username
	assert.Equal(t, "user", approverType,
		"ApproverType should be 'user' (from XML <type>user</type>)")

	assert.NotEmpty(t, approverUserID,
		"FINDING: ApproverUserId is empty — approver username was NOT resolved to a User ID during deploy. "+
			"This is the root cause of 'Approval User not selectable' from confirmed_issue.pdf.")

	if approverUserID != "" {
		assert.NotEqual(t, approverUsername, approverUserID,
			"ApproverUserId should be a resolved User ID, not the raw username string")
	}

	// If we know the current user's ID, verify it matches
	if userID != "" && approverUserID != "" {
		assert.Equal(t, userID, approverUserID,
			"ApproverUserId should match the current user's ID (approver=%s)", approverUsername)
	}
}

func TestAPPR004_RedeployEditPersistsProcessDefinition(t *testing.T) {
	client := liveapi.NewAuthenticatedClient(t)

	userinfoResp, err := client.DoJSON(http.MethodGet, "/services/oauth2/userinfo", nil)
	require.NoError(t, err)

	var userinfo struct {
		Username string `json:"preferred_username"`
		Email    string `json:"email"`
	}
	liveapi.DecodeJSON(t, userinfoResp, &userinfo)

	approverUsername := userinfo.Username
	if approverUsername == "" {
		approverUsername = userinfo.Email
	}
	require.NotEmpty(t, approverUsername, "cannot determine current user's username for redeploy test")

	developerName := fmt.Sprintf("Issue477%d", time.Now().UnixNano())
	fullName := "Account." + developerName
	initialLabel := developerName + " Initial"
	updatedLabel := developerName + " Updated"
	initialDescription := "Initial metadata deployment for issue 477 regression coverage"
	updatedDescription := "Updated metadata deployment for issue 477 regression coverage"

	initialResult := deployApprovalProcessMetadata(t, client, fullName, buildApprovalProcessDeployXML(initialLabel, initialDescription, approverUsername, false))
	requireDeploySucceeded(t, initialResult, "initial approval-process deploy")

	processDef := queryProcessDefinitionByDeveloperName(t, client, developerName)
	require.NotEmpty(t, processDef.ID)

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

	require.Equal(t, initialLabel, processDef.Name)
	require.Equal(t, initialDescription, processDef.Description)
	require.Equal(t, "Inactive", processDef.State)

	redeployResult := deployApprovalProcessMetadata(t, client, fullName, buildApprovalProcessDeployXML(updatedLabel, updatedDescription, approverUsername, true))
	requireDeploySucceeded(t, redeployResult, "approval-process redeploy/edit")

	updatedProcessDef := queryProcessDefinitionByDeveloperName(t, client, developerName)
	require.Equal(t, processDef.ID, updatedProcessDef.ID, "redeploy should update the existing ProcessDefinition")
	assert.Equal(t, updatedLabel, updatedProcessDef.Name)
	assert.Equal(t, updatedDescription, updatedProcessDef.Description)
	assert.Equal(t, "Active", updatedProcessDef.State)
	assert.Contains(t, updatedProcessDef.Metadata, updatedDescription)
	assert.Contains(t, updatedProcessDef.Metadata, "<active>true</active>")
	assert.NotContains(t, updatedProcessDef.Metadata, initialDescription)
}

type deployAsyncResponse struct {
	ID   string `json:"id"`
	Done bool   `json:"done"`
}

type approvalDeployStatusResponse struct {
	DeployResult approvalDeployResult `json:"deployResult"`
}

type approvalDeployResult struct {
	Done         bool                  `json:"done"`
	Status       string                `json:"status"`
	Success      bool                  `json:"success"`
	ErrorMessage string                `json:"errorMessage"`
	Details      approvalDeployDetails `json:"details"`
}

type approvalDeployDetails struct {
	ComponentFailures []approvalComponentFailure `json:"componentFailures"`
}

type approvalComponentFailure struct {
	Problem       string `json:"problem"`
	FileName      string `json:"fileName"`
	ComponentType string `json:"componentType"`
}

type processDefinitionRecord struct {
	ID          string `json:"Id"`
	Name        string `json:"Name"`
	Description string `json:"Description"`
	State       string `json:"State"`
	Metadata    string `json:"Metadata"`
}

func buildApprovalProcessDeployXML(label, description, approverUsername string, active bool) string {
	return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<ApprovalProcess>
  <label>%s</label>
  <active>%t</active>
  <description>%s</description>
  <allowRecall>true</allowRecall>
  <approvalStep>
    <name>Step_1</name>
    <label>Manager Approval</label>
    <assignedApprover>
      <approver>
        <name>%s</name>
        <type>user</type>
      </approver>
      <whenMultipleApprovers>FirstResponse</whenMultipleApprovers>
    </assignedApprover>
  </approvalStep>
</ApprovalProcess>`, xmlEscape(label), active, xmlEscape(description), xmlEscape(approverUsername))
}

func deployApprovalProcessMetadata(t *testing.T, client *liveapi.Client, fullName, approvalXML string) approvalDeployStatusResponse {
	t.Helper()

	zipBytes := buildMetadataDeployZip(t, fullName, approvalXML)
	body, contentType := buildDeployMultipartBody(t, zipBytes)

	resp, err := client.Do(http.MethodPost, "/metadata/deployRequest", contentType, body)
	require.NoError(t, err)

	raw := liveapi.ReadBody(t, resp)
	require.Equal(t, http.StatusCreated, resp.StatusCode, "metadata deploy request failed: %s", strings.TrimSpace(string(raw)))

	var createResp deployAsyncResponse
	require.NoError(t, json.Unmarshal(raw, &createResp))
	require.NotEmpty(t, createResp.ID)

	deadline := time.Now().Add(30 * time.Second)
	for {
		statusResp, pollErr := client.Do(http.MethodGet, "/metadata/deployRequest/"+createResp.ID+"?includeDetails=true", "", nil)
		require.NoError(t, pollErr)

		statusRaw := liveapi.ReadBody(t, statusResp)
		require.Equal(t, http.StatusOK, statusResp.StatusCode, "metadata deploy status failed: %s", strings.TrimSpace(string(statusRaw)))

		var status approvalDeployStatusResponse
		require.NoError(t, json.Unmarshal(statusRaw, &status))
		if status.DeployResult.Done {
			return status
		}

		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for metadata deploy %s", createResp.ID)
		}
		time.Sleep(500 * time.Millisecond)
	}
}

func buildMetadataDeployZip(t *testing.T, fullName, approvalXML string) []byte {
	t.Helper()

	packageXML := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>%s</members>
        <name>ApprovalProcess</name>
    </types>
    <version>63.0</version>
</Package>`, fullName)

	var payload bytes.Buffer
	zipWriter := zip.NewWriter(&payload)

	packageFile, err := zipWriter.Create("package.xml")
	require.NoError(t, err)
	_, err = packageFile.Write([]byte(packageXML))
	require.NoError(t, err)

	componentFile, err := zipWriter.Create("approvalProcesses/" + fullName + ".approvalProcess")
	require.NoError(t, err)
	_, err = componentFile.Write([]byte(approvalXML))
	require.NoError(t, err)

	require.NoError(t, zipWriter.Close())
	return payload.Bytes()
}

func buildDeployMultipartBody(t *testing.T, zipBytes []byte) ([]byte, string) {
	t.Helper()

	var payload bytes.Buffer
	writer := multipart.NewWriter(&payload)

	filePart, err := writer.CreateFormFile("file", "package.zip")
	require.NoError(t, err)
	_, err = filePart.Write(zipBytes)
	require.NoError(t, err)

	require.NoError(t, writer.Close())
	return payload.Bytes(), writer.FormDataContentType()
}

func requireDeploySucceeded(t *testing.T, status approvalDeployStatusResponse, operation string) {
	t.Helper()

	if status.DeployResult.Success {
		return
	}

	failures := make([]string, 0, len(status.DeployResult.Details.ComponentFailures))
	auditFieldFailure := false
	for _, failure := range status.DeployResult.Details.ComponentFailures {
		problem := strings.TrimSpace(failure.Problem)
		if problem == "" {
			continue
		}
		failures = append(failures, problem)
		lowerProblem := strings.ToLower(problem)
		if strings.Contains(lowerProblem, "lastmodifieddate") ||
			strings.Contains(lowerProblem, "lastmodifiedbyid") ||
			strings.Contains(lowerProblem, "system audit field") {
			auditFieldFailure = true
		}
	}

	if auditFieldFailure {
		t.Fatalf("FINDING: %s failed because the redeploy/edit path is still attempting to set system audit fields: %s", operation, strings.Join(failures, "; "))
	}

	t.Fatalf("%s failed: status=%s error=%s failures=%s", operation, status.DeployResult.Status, status.DeployResult.ErrorMessage, strings.Join(failures, "; "))
}

func queryProcessDefinitionByDeveloperName(t *testing.T, client *liveapi.Client, developerName string) processDefinitionRecord {
	t.Helper()

	soql := fmt.Sprintf(
		"SELECT Id,Name,Description,State,Metadata FROM ProcessDefinition WHERE DeveloperName='%s' AND TableEnumOrId='Account'",
		developerName,
	)
	queryResp, err := client.Do(http.MethodGet, "/query?q="+url.QueryEscape(soql), "", nil)
	require.NoError(t, err)

	queryBody := liveapi.ReadBody(t, queryResp)
	require.Equal(t, http.StatusOK, queryResp.StatusCode, "ProcessDefinition query failed: %s", strings.TrimSpace(string(queryBody)))

	var result struct {
		TotalSize int                       `json:"totalSize"`
		Records   []processDefinitionRecord `json:"records"`
	}
	require.NoError(t, json.Unmarshal(queryBody, &result))
	require.NotZero(t, result.TotalSize, "ProcessDefinition not found for DeveloperName=%s", developerName)
	require.NotEmpty(t, result.Records)
	return result.Records[0]
}

func xmlEscape(value string) string {
	replacer := strings.NewReplacer(
		"&", "&amp;",
		"<", "&lt;",
		">", "&gt;",
		`"`, "&quot;",
		"'", "&apos;",
	)
	return replacer.Replace(value)
}
