package rest

import (
	"context"
	"fmt"
	"testing"

	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type fakeApprovalProcessPersistence struct {
	inserted         []insertCall
	deleted          []deleteCall
	physicalDeleted  []deleteCall
	lookupByUsername map[string]models.SObject
}

type insertCall struct {
	objectName string
	record     models.SObject
}

type deleteCall struct {
	objectName string
	id         string
}

func (f *fakeApprovalProcessPersistence) RunInTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
	return fn(ctx)
}

func (f *fakeApprovalProcessPersistence) Insert(_ context.Context, objectName string, data models.SObject, _ *models.UserSession) (models.SObject, error) {
	cloned := cloneSObject(data)
	f.inserted = append(f.inserted, insertCall{objectName: objectName, record: cloned})
	if _, ok := cloned[constants.FieldID]; !ok {
		cloned[constants.FieldID] = fmt.Sprintf("%s-%d", objectName, len(f.inserted))
	}
	return cloned, nil
}

func (f *fakeApprovalProcessPersistence) Update(_ context.Context, _ string, _ string, _ models.SObject, _ *models.UserSession) error {
	return nil
}

func (f *fakeApprovalProcessPersistence) Delete(_ context.Context, objectName string, id string, _ *models.UserSession) error {
	f.deleted = append(f.deleted, deleteCall{objectName: objectName, id: id})
	return nil
}

func (f *fakeApprovalProcessPersistence) PhysicalDeleteRaw(_ context.Context, objectName string, id string) error {
	f.physicalDeleted = append(f.physicalDeleted, deleteCall{objectName: objectName, id: id})
	return nil
}

func (f *fakeApprovalProcessPersistence) RetrieveByField(_ context.Context, objectName string, _ string, fieldValue string, _ *models.UserSession) (models.SObject, error) {
	if objectName != constants.TableUser {
		return nil, nil
	}
	if record, ok := f.lookupByUsername[fieldValue]; ok {
		return cloneSObject(record), nil
	}
	return nil, nil
}

type fakeApprovalProcessQueryService struct {
	results map[string]*models.QueryResult
}

func (f *fakeApprovalProcessQueryService) Query(context.Context, api.QueryRequest, *models.UserSession) (*models.QueryResult, error) {
	return nil, nil
}

func (f *fakeApprovalProcessQueryService) QueryWithFilter(_ context.Context, objectName string, _ string, _ *models.UserSession, _ string, _ string, _ int) (*models.QueryResult, error) {
	if result, ok := f.results[objectName]; ok {
		return result, nil
	}
	return &models.QueryResult{}, nil
}

func (f *fakeApprovalProcessQueryService) RunAnalytics(context.Context, models.AnalyticsQuery, *models.UserSession) (interface{}, error) {
	return nil, nil
}

func (f *fakeApprovalProcessQueryService) SearchSingleObject(context.Context, string, string, []string, []string, int, int, string, string, string, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (f *fakeApprovalProcessQueryService) GlobalSearch(context.Context, string, *models.UserSession) ([]models.SearchResult, error) {
	return nil, nil
}

func (f *fakeApprovalProcessQueryService) Calculate(context.Context, string, models.SObject, *models.UserSession) (models.SObject, error) {
	return nil, nil
}

func (f *fakeApprovalProcessQueryService) ExecuteRawSQL(context.Context, string, []interface{}, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func TestApprovalProcessMetadataPayload(t *testing.T) {
	validXML := `<?xml version="1.0" encoding="UTF-8"?><ApprovalProcess><label>Example</label><active>true</active></ApprovalProcess>`

	raw, ok := approvalProcessMetadataPayload(constants.TableProcessDefinition, map[string]interface{}{
		constants.FieldProcessDefinition_Metadata: validXML,
	})
	require.True(t, ok)
	assert.Equal(t, validXML, raw)

	_, ok = approvalProcessMetadataPayload(constants.TableProcessDefinition, map[string]interface{}{
		constants.FieldProcessDefinition_Metadata: "<not-approval/>",
	})
	assert.False(t, ok)

	_, ok = approvalProcessMetadataPayload("Account", map[string]interface{}{
		constants.FieldProcessDefinition_Metadata: validXML,
	})
	assert.False(t, ok)
}

func TestBuildApprovalSyncProcessNodeRecord_DefaultsRejectBehaviorAndStepOrder(t *testing.T) {
	record := buildApprovalSyncProcessNodeRecord(ApprovalStepXML{
		Name:        "Step1",
		Description: "Default reject behavior test",
	}, "pd-1", 3)

	assert.Equal(t, "Step1", record["Name"])
	assert.Equal(t, "Step1", record["DeveloperName"])
	assert.Equal(t, "pd-1", record["ProcessDefinitionId"])
	assert.Equal(t, 3, record["StepOrder"])
	assert.Equal(t, "FinalRejection", record["RejectBehavior"])
	assert.Equal(t, true, record["AllowComments"])
	assert.Equal(t, false, record["RequireComments"])
}

func TestSyncApprovalProcessChildren_ReplacesExistingChildrenAndResolvesApprover(t *testing.T) {
	persistence := &fakeApprovalProcessPersistence{
		lookupByUsername: map[string]models.SObject{
			"manager@test.com": {constants.FieldID: "005-resolved"},
		},
	}
	querySvc := &fakeApprovalProcessQueryService{
		results: map[string]*models.QueryResult{
			constants.TableProcessNode: {
				Records: []models.SObject{{constants.FieldID: "node-old"}},
			},
			constants.TableProcessAction: {
				Records: []models.SObject{{constants.FieldID: "action-old"}},
			},
		},
	}

	rawXML := `<?xml version="1.0" encoding="UTF-8"?>
<ApprovalProcess>
  <label>Example Approval</label>
  <active>true</active>
  <approvalStep>
    <name>Step1</name>
    <label>Manager Approval</label>
    <description>Needs approval</description>
    <assignedApprover>
      <approver>
        <name>manager@test.com</name>
        <type>user</type>
      </approver>
    </assignedApprover>
    <stepApprovalActions>
      <actions>
        <name>MarkApproved</name>
        <type>FieldUpdate</type>
        <fieldApiName>Status</fieldApiName>
        <fieldValue>Approved</fieldValue>
      </actions>
    </stepApprovalActions>
    <stepRejectionActions>
      <actions>
        <name>MarkRejected</name>
        <type>FieldUpdate</type>
        <fieldApiName>Status</fieldApiName>
        <fieldValue>Rejected</fieldValue>
      </actions>
    </stepRejectionActions>
  </approvalStep>
  <initialSubmissionActions>
    <actions>
      <name>SetStatus</name>
      <type>FieldUpdate</type>
      <fieldApiName>Status</fieldApiName>
      <fieldValue>Submitted</fieldValue>
    </actions>
  </initialSubmissionActions>
  <stepApprovalActions>
    <actions>
      <name>NotifyNextApprover</name>
      <type>Alert</type>
      <emailTemplateId>tmpl-step</emailTemplateId>
    </actions>
  </stepApprovalActions>
  <reassignActions>
    <actions>
      <name>NotifyReassignedApprover</name>
      <type>Alert</type>
      <emailTemplateId>tmpl-reassign</emailTemplateId>
    </actions>
  </reassignActions>
</ApprovalProcess>`

	var failures []ComponentFailure
	err := syncApprovalProcessChildren(
		context.Background(),
		persistence,
		querySvc,
		"pd-1",
		rawXML,
		"approvalProcesses/Account.Example.approvalProcess",
		&models.UserSession{ID: "user-1"},
		func(failure ComponentFailure) {
			failures = append(failures, failure)
		},
	)
	require.NoError(t, err)
	assert.Empty(t, failures)
	assert.Equal(t, []deleteCall{
		{objectName: constants.TableProcessAction, id: "action-old"},
		{objectName: constants.TableProcessNode, id: "node-old"},
	}, persistence.physicalDeleted)
	assert.Empty(t, persistence.deleted)

	insertedNode := findInsertedRecord(t, persistence.inserted, constants.TableProcessNode)
	assert.Equal(t, "pd-1", insertedNode["ProcessDefinitionId"])
	assert.Equal(t, "user", insertedNode["ApproverType"])
	assert.Equal(t, "005-resolved", insertedNode["ApproverUserId"])
	assert.Equal(t, 1, insertedNode["StepOrder"])
	assert.Equal(t, "FinalRejection", insertedNode["RejectBehavior"])
	assertNoAuditFields(t, insertedNode)

	insertedActions := findInsertedRecords(t, persistence.inserted, constants.TableProcessAction)
	require.Len(t, insertedActions, 5)

	assert.Equal(t, "StepApproval", insertedActions[0]["ActionEvent"])
	assert.Equal(t, "ProcessNode-1", insertedActions[0]["ProcessNodeId"])
	assert.Equal(t, "Status", insertedActions[0]["FieldApiName"])
	assert.Equal(t, "Approved", insertedActions[0]["FieldValue"])
	assertNoAuditFields(t, insertedActions[0])

	assert.Equal(t, "StepRejection", insertedActions[1]["ActionEvent"])
	assert.Equal(t, "ProcessNode-1", insertedActions[1]["ProcessNodeId"])
	assert.Equal(t, "Rejected", insertedActions[1]["FieldValue"])
	assertNoAuditFields(t, insertedActions[1])

	assert.Equal(t, "Submit", insertedActions[2]["ActionEvent"])
	assert.Equal(t, "FieldUpdate", insertedActions[2]["ActionType"])
	assert.Equal(t, "Submitted", insertedActions[2]["FieldValue"])
	_, hasNodeID := insertedActions[2]["ProcessNodeId"]
	assert.False(t, hasNodeID)
	assertNoAuditFields(t, insertedActions[2])

	assert.Equal(t, "StepApproval", insertedActions[3]["ActionEvent"])
	assert.Equal(t, "Alert", insertedActions[3]["ActionType"])
	assert.Equal(t, "tmpl-step", insertedActions[3]["EmailTemplateId"])
	assertNoAuditFields(t, insertedActions[3])

	assert.Equal(t, "Reassign", insertedActions[4]["ActionEvent"])
	assert.Equal(t, "Alert", insertedActions[4]["ActionType"])
	assert.Equal(t, "tmpl-reassign", insertedActions[4]["EmailTemplateId"])
	assertNoAuditFields(t, insertedActions[4])
}

func TestSyncApprovalProcessChildren_UnresolvedApproverIsNonFatal(t *testing.T) {
	persistence := &fakeApprovalProcessPersistence{
		lookupByUsername: map[string]models.SObject{},
	}
	querySvc := &fakeApprovalProcessQueryService{results: map[string]*models.QueryResult{}}

	rawXML := `<?xml version="1.0" encoding="UTF-8"?>
<ApprovalProcess>
  <label>Example Approval</label>
  <active>true</active>
  <approvalStep>
    <name>Step1</name>
    <label>Manager Approval</label>
    <assignedApprover>
      <approver>
        <name>missing@test.com</name>
        <type>user</type>
      </approver>
    </assignedApprover>
  </approvalStep>
</ApprovalProcess>`

	var failures []ComponentFailure
	err := syncApprovalProcessChildren(
		context.Background(),
		persistence,
		querySvc,
		"pd-2",
		rawXML,
		"approvalProcesses/Account.Missing.approvalProcess",
		&models.UserSession{ID: "user-1"},
		func(failure ComponentFailure) {
			failures = append(failures, failure)
		},
	)
	require.NoError(t, err)
	require.Len(t, failures, 1)
	assert.Contains(t, failures[0].Problem, "missing@test.com")
	assert.Equal(t, "approvalProcesses/Account.Missing.approvalProcess", failures[0].FileName)

	insertedNode := findInsertedRecord(t, persistence.inserted, constants.TableProcessNode)
	_, hasApproverUserID := insertedNode["ApproverUserId"]
	assert.False(t, hasApproverUserID)
	assert.Equal(t, "user", insertedNode["ApproverType"])
}

func TestSyncApprovalProcessChildren_PersistsHierarchyApproverType(t *testing.T) {
	persistence := &fakeApprovalProcessPersistence{
		lookupByUsername: map[string]models.SObject{},
	}
	querySvc := &fakeApprovalProcessQueryService{
		results: map[string]*models.QueryResult{
			constants.TableProcessNode:   {Records: []models.SObject{}},
			constants.TableProcessAction: {Records: []models.SObject{}},
		},
	}

	rawXML := `<?xml version="1.0" encoding="UTF-8"?>
<ApprovalProcess>
  <label>Hierarchy Approval</label>
  <active>true</active>
  <nextAutomatedApprover>
    <useApproverFieldOfRecordOwner>false</useApproverFieldOfRecordOwner>
    <userHierarchyField>ManagerId</userHierarchyField>
  </nextAutomatedApprover>
  <approvalStep>
    <name>ManagerStep</name>
    <label>Manager Step</label>
    <assignedApprover>
      <approver>
        <type>hierarchy</type>
      </approver>
    </assignedApprover>
  </approvalStep>
</ApprovalProcess>`

	err := syncApprovalProcessChildren(
		context.Background(),
		persistence,
		querySvc,
		"pd-1",
		rawXML,
		"approvalProcesses/Account.Hierarchy.approvalProcess",
		&models.UserSession{ID: "user-1"},
		nil,
	)
	require.NoError(t, err)

	insertedNode := findInsertedRecord(t, persistence.inserted, constants.TableProcessNode)
	assert.Equal(t, "hierarchy", insertedNode["ApproverType"])
	_, hasApproverUserID := insertedNode["ApproverUserId"]
	assert.False(t, hasApproverUserID)
}

func findInsertedRecord(t *testing.T, calls []insertCall, objectName string) models.SObject {
	t.Helper()
	for _, call := range calls {
		if call.objectName == objectName {
			return call.record
		}
	}
	t.Fatalf("no inserted record found for %s", objectName)
	return nil
}

func findInsertedRecords(t *testing.T, calls []insertCall, objectName string) []models.SObject {
	t.Helper()
	var records []models.SObject
	for _, call := range calls {
		if call.objectName == objectName {
			records = append(records, call.record)
		}
	}
	if len(records) == 0 {
		t.Fatalf("no inserted records found for %s", objectName)
	}
	return records
}

func assertNoAuditFields(t *testing.T, record models.SObject) {
	t.Helper()
	for _, forbidden := range []string{"CreatedDate", "CreatedById", "LastModifiedDate", "LastModifiedById"} {
		if _, exists := record[forbidden]; exists {
			t.Fatalf("record unexpectedly contains %s: %#v", forbidden, record[forbidden])
		}
	}
}

