package dialectsfcrm

import (
	"bytes"
	"context"
	"errors"
	"log"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestProcessApprovalsNeutralHandlerProjectsWrapperResults(t *testing.T) {
	approvals := &fakeApprovals{
		recallRecord: contract.Record{
			"ProcessInstanceId": "pi-1",
			"TargetObjectId":    "00Qcanon",
			"ActionFailures": []contract.ApprovalProcessActionFailure{{
				ActionName: "Recall Unknown Action",
				ActionType: "UnsupportedAction",
				Reason:     "unknown ActionType \"UnsupportedAction\"",
			}},
		},
	}

	resp, err := ProcessApprovalsNeutralHandler(context.Background(), &dialect.Request{
		User: testUser(),
		Body: []byte(`{"requests":[{"actionType":"Removed","contextId":"wi-recall"}]}`),
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "approval-wrapper-result", resp.Body, "", `[{"actorIds":["user-1"],"entityId":"00Qcanon","errors":[],"instanceId":"pi-1","instanceStatus":"Removed","newWorkitemIds":[],"success":true,"actionFailures":[{"actionName":"Recall Unknown Action","actionType":"UnsupportedAction","reason":"unknown ActionType \"UnsupportedAction\""}]}]`)
}

func TestProcessApprovalsNeutralHandlerLegacySingleSubmitErrorsUseV63Envelope(t *testing.T) {
	for _, tc := range []struct {
		name       string
		err        error
		wantStatus int
		wantCode   string
	}{
		{name: "not found", err: approvalStatusError{status: http.StatusNotFound, msg: "record not found"}, wantStatus: http.StatusNotFound, wantCode: wire.SFCodeNotFound},
		{name: "permission", err: approvalStatusError{status: http.StatusForbidden, msg: "permission denied"}, wantStatus: http.StatusForbidden, wantCode: wire.SFCodeForbidden},
		{name: "conflict", err: approvalStatusError{status: http.StatusConflict, msg: "approval already pending"}, wantStatus: http.StatusConflict, wantCode: wire.SFCodeConflict},
		{name: "validation", err: approvalStatusError{status: http.StatusBadRequest, msg: "entry criteria not met"}, wantStatus: http.StatusBadRequest, wantCode: wire.SFCodeInvalidRequest},
		{name: "internal", err: approvalInternalError{msg: "raw sql exploded"}, wantStatus: http.StatusInternalServerError, wantCode: wire.SFCodeInternalError},
	} {
		t.Run(tc.name, func(t *testing.T) {
			resp, err := ProcessApprovalsNeutralHandler(context.Background(), &dialect.Request{
				User: testUser(),
				Body: []byte(`{"actionType":"Submit","contextId":"00Qtarget"}`),
			}, approvalDeps(&fakeApprovals{submitErr: tc.err}))
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if resp.Status != tc.wantStatus {
				t.Fatalf("status = %d, want %d; body=%#v", resp.Status, tc.wantStatus, resp.Body)
			}
			assertSFError(t, resp, tc.wantStatus, tc.wantCode)
			got := mustJSON(t, resp.Body)
			if strings.Contains(got, "raw sql exploded") {
				t.Fatalf("internal error leaked: %s", got)
			}
		})
	}
}

func TestProcessApprovalsNeutralHandlerLegacySingleSubmitInternalErrorUsesCanonicalGenericAndLogs(t *testing.T) {
	var logs bytes.Buffer
	previousLogOutput := log.Writer()
	log.SetOutput(&logs)
	t.Cleanup(func() { log.SetOutput(previousLogOutput) })

	resp, err := ProcessApprovalsNeutralHandler(context.Background(), &dialect.Request{
		User: testUser(),
		Body: []byte(`{"actionType":"Submit","contextId":"00Qtarget"}`),
	}, approvalDeps(&fakeApprovals{submitErr: approvalInternalError{msg: "raw sql exploded"}}))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	if got, want := mustJSON(t, resp.Body), mustJSON(t, wire.SFGenericInternalErrorResponse()); got != want {
		t.Fatalf("body = %s, want canonical generic %s", got, want)
	}
	if strings.Contains(mustJSON(t, resp.Body), "raw sql exploded") {
		t.Fatalf("internal error leaked: %s", mustJSON(t, resp.Body))
	}
	if !strings.Contains(logs.String(), "raw sql exploded") {
		t.Fatalf("internal cause was not logged: %s", logs.String())
	}
}

func TestApprovalHTTPStatus5xxUsesCanonicalGenericAndLogs(t *testing.T) {
	for _, tc := range []struct {
		name   string
		status int
	}{
		{name: "internal", status: http.StatusInternalServerError},
		{name: "unavailable", status: http.StatusServiceUnavailable},
	} {
		t.Run(tc.name, func(t *testing.T) {
			var logs bytes.Buffer
			previousLogOutput := log.Writer()
			log.SetOutput(&logs)
			t.Cleanup(func() { log.SetOutput(previousLogOutput) })

			resp := approvalHTTPStatusErrorResponse(approvalStatusError{status: tc.status, msg: "database pool exhausted"})
			if resp == nil {
				t.Fatal("response is nil")
			}
			if resp.Status != tc.status {
				t.Fatalf("status = %d, want %d; body=%#v", resp.Status, tc.status, resp.Body)
			}
			if got, want := mustJSON(t, resp.Body), mustJSON(t, wire.SFGenericInternalErrorResponse()); got != want {
				t.Fatalf("body = %s, want canonical generic %s", got, want)
			}
			if strings.Contains(mustJSON(t, resp.Body), "database pool exhausted") {
				t.Fatalf("internal status error leaked: %s", mustJSON(t, resp.Body))
			}
			if !strings.Contains(logs.String(), "database pool exhausted") {
				t.Fatalf("internal status error was not logged: %s", logs.String())
			}
		})
	}
}

func TestProcessApprovalsNeutralHandlerWrapperInternalErrorsStayBatchResults(t *testing.T) {
	resp, err := ProcessApprovalsNeutralHandler(context.Background(), &dialect.Request{
		User: testUser(),
		Body: []byte(`{"requests":[{"actionType":"Submit","contextId":"00Qone"},{"actionType":"Submit","contextId":"00Qtwo"}]}`),
	}, approvalDeps(&fakeApprovals{submitErr: approvalInternalError{msg: "top-secret db detail"}}))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	got := mustJSON(t, resp.Body)
	if strings.Contains(got, "top-secret") {
		t.Fatalf("batch internal error leaked: %s", got)
	}
	assertBody(t, "approval-wrapper-internal", resp.Body, "", `[{"actorIds":[],"entityId":"00Qone","errors":["An unexpected error occurred"],"instanceId":"","instanceStatus":"Error","newWorkitemIds":[],"success":false},{"actorIds":[],"entityId":"00Qtwo","errors":["An unexpected error occurred"],"instanceId":"","instanceStatus":"Error","newWorkitemIds":[],"success":false}]`)
}

func TestApprovalReadHelpersProjectV63DTOs(t *testing.T) {
	approvals := &fakeApprovals{
		pending: []contract.ApprovalPendingView{{
			WorkItemID:          "wi-1",
			ProcessInstanceID:   "pi-1",
			ProcessDefinitionID: "pd-1",
			TargetObjectID:      "001-record",
			ObjectAPIName:       "Account",
			SubmittedByID:       "005-submit",
			SubmittedByName:     "Submitter",
			SubmittedDate:       "2026-06-12T10:00:00Z",
			ActorID:             "005-actor",
			OriginalActorID:     "005-original",
			Comments:            "please review",
			RecordName:          "Acme",
		}},
		summary: &contract.ApprovalProcessInstanceSummary{
			ID:                  "pi-1",
			ProcessDefinitionID: "pd-1",
			TargetObjectID:      "001-record",
			Status:              constants.ApprovalStatusPending,
			SubmittedByID:       "005-submit",
			SubmittedByName:     "Submitter",
			CreatedDate:         "2026-06-12T10:00:00Z",
			PendingWorkItems: []contract.ApprovalProcessInstancePendingWorkItem{{
				ID:              "wi-1",
				ActorID:         "005-actor",
				ActorName:       "Approver",
				OriginalActorID: "005-original",
			}},
		},
		steps: []contract.ApprovalStepDisplayMetadata{{
			ProcessNodeID:    "pn-1",
			StepOrder:        2,
			StepName:         "Manager_Approval",
			StepLabel:        "Manager Approval",
			HasEntryCriteria: true,
			IfCriteriaNotMet: "ApproveRecord",
		}},
	}

	resp, err := ApprovalPendingNeutralHandler(context.Background(), &dialect.Request{User: testUser()}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("pending unexpected error: %v", err)
	}
	assertBody(t, "approval-pending", resp.Body, "", `[{"workItemId":"wi-1","processInstanceId":"pi-1","processDefinitionId":"pd-1","targetObjectId":"001-record","objectApiName":"Account","submittedById":"005-submit","submittedByName":"Submitter","submittedDate":"2026-06-12T10:00:00Z","actorId":"005-actor","originalActorId":"005-original","comments":"please review","recordName":"Acme"}]`)

	resp, err = ApprovalLatestProcessInstanceNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"recordId": "001-record"},
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("summary unexpected error: %v", err)
	}
	assertBody(t, "approval-summary", resp.Body, "", `{"id":"pi-1","processDefinitionId":"pd-1","targetObjectId":"001-record","status":"Pending","submittedById":"005-submit","submittedByName":"Submitter","createdDate":"2026-06-12T10:00:00Z","pendingWorkItems":[{"id":"wi-1","actorId":"005-actor","actorName":"Approver","originalActorId":"005-original"}]}`)

	resp, err = ApprovalStepDisplayMetadataNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"recordId": "001-record"},
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("steps unexpected error: %v", err)
	}
	assertBody(t, "approval-steps", resp.Body, "", `[{"processNodeId":"pn-1","stepOrder":2,"stepName":"Manager_Approval","stepLabel":"Manager Approval","hasEntryCriteria":true,"ifCriteriaNotMet":"ApproveRecord"}]`)
}

func TestApprovalNotificationFlowsAndWorkflowStubs(t *testing.T) {
	approvals := &fakeApprovals{
		notificationFlows: []contract.FlowDescriptor{{
			Label:            "Notify Approver",
			Name:             "Notify_Approver",
			Description:      "approval email",
			ActiveRevisionID: "301-flow",
		}},
	}
	resp, err := ApprovalNotificationFlowsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v62.0",
		User:    testUser(),
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("notification unexpected error: %v", err)
	}
	assertBody(t, "approval-notification-flows", resp.Body, "", `{"actions":[{"label":"Notify Approver","name":"Notify_Approver","type":"FLOW","url":"/services/data/v62.0/actions/custom/flow/Notify_Approver","description":"approval email"}]}`)

	resp, err = WorkflowRulesNeutralHandler(context.Background(), &dialect.Request{User: testUser()}, dialect.Deps{})
	if err != nil {
		t.Fatalf("rules unexpected error: %v", err)
	}
	assertBody(t, "workflow-rules", resp.Body, "", `{"rules":{}}`)

	resp, err = WorkflowRulesForObjectNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"sObject": "Account"},
	}, dialect.Deps{})
	if err != nil {
		t.Fatalf("object rules unexpected error: %v", err)
	}
	assertBody(t, "workflow-rules-object", resp.Body, "", `{"rules":[]}`)
}

func TestApprovalCloneNeutralHandlerValidatesAndDelegates(t *testing.T) {
	approvals := &fakeApprovals{
		cloneRecord: contract.Record{
			constants.FieldID: "pd-clone",
			"DeveloperName":   "Cloned_Process",
			"Name":            "Cloned Process",
		},
	}
	resp, err := ApprovalCloneNeutralHandler(context.Background(), &dialect.Request{
		User: testUser(),
		Body: []byte(`{"processDefinitionId":"pd-1","newDeveloperName":"Cloned_Process","newLabel":"Cloned Process"}`),
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("clone unexpected error: %v", err)
	}
	if approvals.cloneProcessDefID != "pd-1" || approvals.cloneDeveloperName != "Cloned_Process" || approvals.cloneLabel != "Cloned Process" {
		t.Fatalf("clone args = %q %q %q", approvals.cloneProcessDefID, approvals.cloneDeveloperName, approvals.cloneLabel)
	}
	assertBody(t, "approval-clone", resp.Body, "", `{"DeveloperName":"Cloned_Process","Id":"pd-clone","Name":"Cloned Process"}`)

	resp, err = ApprovalCloneNeutralHandler(context.Background(), &dialect.Request{
		User: testUser(),
		Body: []byte(`{"processDefinitionId":"pd-1"}`),
	}, approvalDeps(approvals))
	if err != nil {
		t.Fatalf("clone validation unexpected error: %v", err)
	}
	assertSFError(t, resp, http.StatusBadRequest, wire.SFCodeInvalidRequest)
}

func approvalDeps(approvals contract.Approvals) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{Approvals: approvals}}
}

type fakeApprovals struct {
	submitRecord contract.Record
	submitErr    error
	recallRecord contract.Record
	recallErr    error

	pending []contract.ApprovalPendingView
	summary *contract.ApprovalProcessInstanceSummary
	steps   []contract.ApprovalStepDisplayMetadata

	notificationFlows []contract.FlowDescriptor

	cloneRecord        contract.Record
	cloneProcessDefID  string
	cloneDeveloperName string
	cloneLabel         string
}

func (f *fakeApprovals) Submit(context.Context, contract.ApprovalSubmitRequest, *models.UserSession) (contract.Record, error) {
	return f.submitRecord, f.submitErr
}

func (f *fakeApprovals) Approve(context.Context, string, string, *models.UserSession) (contract.Record, error) {
	return nil, errors.New("unexpected approve call")
}

func (f *fakeApprovals) Reject(context.Context, string, string, *models.UserSession) (contract.Record, error) {
	return nil, errors.New("unexpected reject call")
}

func (f *fakeApprovals) Recall(context.Context, string, string, *models.UserSession) (contract.Record, error) {
	return f.recallRecord, f.recallErr
}

func (f *fakeApprovals) Reassign(context.Context, string, string, *models.UserSession) error {
	return nil
}

func (f *fakeApprovals) ListPendingApprovals(context.Context, *models.UserSession) ([]contract.ApprovalPendingView, error) {
	return f.pending, nil
}

func (f *fakeApprovals) GetLatestProcessInstance(context.Context, string, *models.UserSession) (*contract.ApprovalProcessInstanceSummary, error) {
	return f.summary, nil
}

func (f *fakeApprovals) GetRecordApprovalStepDisplayMetadata(context.Context, string, *models.UserSession) ([]contract.ApprovalStepDisplayMetadata, error) {
	return f.steps, nil
}

func (f *fakeApprovals) Clone(_ context.Context, processDefID, newDeveloperName, newLabel string, _ *models.UserSession) (contract.Record, error) {
	f.cloneProcessDefID = processDefID
	f.cloneDeveloperName = newDeveloperName
	f.cloneLabel = newLabel
	return f.cloneRecord, nil
}

func (f *fakeApprovals) ListApprovalNotificationFlows(context.Context) ([]contract.FlowDescriptor, error) {
	return f.notificationFlows, nil
}

type approvalStatusError struct {
	status int
	msg    string
}

func (e approvalStatusError) Error() string { return e.msg }

func (e approvalStatusError) HTTPStatus() int { return e.status }

type approvalInternalError struct {
	msg string
}

func (e approvalInternalError) Error() string { return e.msg }

func (approvalInternalError) InternalCondition() bool { return true }
