package dialectsfcrm

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

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

func TestListInvocableFlowsUsesNeutralCatalog(t *testing.T) {
	catalog := &fakeFlowCatalog{
		protected: []string{"HiddenFlow"},
		flows: []contract.FlowDescriptor{{
			Label:            "Do Thing",
			Name:             "DoThing",
			Description:      "runs a thing",
			ActiveRevisionID: "flow-v1",
		}, {
			Label:            "Hidden Flow",
			Name:             "HiddenFlow",
			ActiveRevisionID: "flow-v2",
		}, {
			Label: "Draft Only",
			Name:  "DraftOnly",
		}},
	}
	resp, err := ListInvocableFlowsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v62.0",
		User:    testUser(),
	}, flowActionDeps(nil, catalog))
	if err != nil {
		t.Fatalf("ListInvocableFlowsNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if !catalog.listCalled {
		t.Fatal("list catalog call was not observed")
	}
	assertBody(t, "flow-list", resp.Body, "",
		`{"actions":[{"label":"Do Thing","name":"DoThing","type":"FLOW","url":"/services/data/v62.0/actions/custom/flow/DoThing","description":"runs a thing"}]}`)
}

func TestGetFlowSchemaUsesNeutralCatalog(t *testing.T) {
	catalog := &fakeFlowCatalog{
		flow: &contract.FlowDescriptor{
			Label:            "Do Thing",
			Name:             "DoThing",
			ActiveRevisionID: "flow-v1",
		},
		version: &contract.FlowRevisionDescriptor{
			Variables: []contract.FlowVariableDescriptor{
				{Name: "recordId", DataType: "String", IsInput: true},
				{Name: "resultId", DataType: "String", IsOutput: true},
			},
		},
	}
	resp, err := GetFlowSchemaNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "DoThing"},
	}, flowActionDeps(nil, catalog))
	if err != nil {
		t.Fatalf("GetFlowSchemaNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if catalog.schemaName != "DoThing" {
		t.Fatalf("schemaName = %q, want DoThing", catalog.schemaName)
	}
	if catalog.versionID != "flow-v1" {
		t.Fatalf("versionID = %q, want flow-v1", catalog.versionID)
	}
	assertBody(t, "flow-schema", resp.Body, "",
		`{"label":"Do Thing","name":"DoThing","type":"FLOW","url":"/services/data/v63.0/actions/custom/flow/DoThing","inputs":[{"name":"recordId","label":"recordId","dataType":"String"}],"outputs":[{"name":"resultId","label":"resultId","dataType":"String"}]}`)
}

func TestGetFlowSchemaHidesMissingOrProtectedFlow(t *testing.T) {
	resp, err := GetFlowSchemaNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "ProtectedFlow"},
	}, flowActionDeps(nil, &fakeFlowCatalog{
		protected: []string{"ProtectedFlow"},
		flow:      &contract.FlowDescriptor{Name: "ProtectedFlow", ActiveRevisionID: "flow-v1"},
	}))
	if err != nil {
		t.Fatalf("GetFlowSchemaNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, "Flow"))
	assertBody(t, "flow-schema-missing", resp.Body, want, want)
}

func TestListInvocableFlowsSanitizesCatalogErrors(t *testing.T) {
	resp, err := ListInvocableFlowsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
	}, flowActionDeps(nil, &fakeFlowCatalog{err: errors.New("select secret")}))
	if err != nil {
		t.Fatalf("ListInvocableFlowsNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeInternalError, flowActionGenericInternalErrorMessage))
	assertBody(t, "flow-list-catalog-error", resp.Body, want, want)
}

func TestExecuteCustomFlowUsesNeutralExecutor(t *testing.T) {
	executor := &fakeFlowExecutor{
		result: &contract.FlowExecutionResult{
			ActionName:   "DoThing",
			IsSuccess:    true,
			OutputValues: map[string]any{"answer": 42},
		},
	}
	resp, err := ExecuteCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "DoThing"},
		Body:    []byte(`{"inputs":[{"recordId":"001xx0000000001AAA"}]}`),
	}, flowActionDeps(executor, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ExecuteCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if executor.flowName != "DoThing" || executor.inputs["recordId"] != "001xx0000000001AAA" || executor.user.ID != testUser().ID {
		t.Fatalf("executor call = flowName %q inputs %#v user %#v", executor.flowName, executor.inputs, executor.user)
	}
	assertBody(t, "flow-execute-success", resp.Body, "",
		`[{"actionName":"DoThing","isSuccess":true,"outputValues":{"answer":42}}]`)
}

func TestExecuteCustomFlowProjectsNeutralFieldWriteDenialToV63(t *testing.T) {
	executor := &fakeFlowExecutor{
		result: &contract.FlowExecutionResult{
			ActionName:    "DoThing",
			IsSuccess:     false,
			IsCustomError: true,
			Errors: []contract.FlowExecutionError{{
				Code:    contract.FlowExecutionErrorCodeFieldWriteDenied,
				Message: "field is not writable",
				Fields:  []string{"Secret__c"},
			}},
		},
	}
	resp, err := ExecuteCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "DoThing"},
		Body:    []byte(`{"inputs":[{}]}`),
	}, flowActionDeps(executor, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ExecuteCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "flow-execute-field-write-denied", resp.Body, "",
		`[{"actionName":"DoThing","isSuccess":false,"errors":[{"statusCode":"INVALID_FIELD_FOR_INSERT_UPDATE","message":"field is not writable","fields":["Secret__c"]}]}]`)
}

func TestExecuteCustomFlowHidesProtectedFlow(t *testing.T) {
	executor := &fakeFlowExecutor{}
	resp, err := ExecuteCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "ProtectedFlow"},
		Body:    []byte(`{"inputs":[{}]}`),
	}, flowActionDeps(executor, &fakeFlowCatalog{protected: []string{"protectedflow"}}))
	if err != nil {
		t.Fatalf("ExecuteCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
	}
	if executor.called {
		t.Fatal("protected flow reached executor")
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, "Flow"))
	assertBody(t, "flow-execute-protected", resp.Body, want, want)
}

func TestExecuteCustomFlowSanitizesInternalPerInputError(t *testing.T) {
	resp, err := ExecuteCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "DoThing"},
		Body:    []byte(`{"inputs":[{}]}`),
	}, flowActionDeps(&fakeFlowExecutor{err: sensitiveFlowInternalError("secret dsn")}, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ExecuteCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	body := mustJSON(t, resp.Body)
	if strings.Contains(body, "secret dsn") {
		t.Fatalf("internal detail leaked in body: %s", body)
	}
	assertBody(t, "flow-execute-internal", resp.Body, "",
		`[{"actionName":"DoThing","isSuccess":false,"errors":[{"statusCode":"FLOW_EXECUTION_ERROR","message":"An internal server error occurred. Contact your system administrator."}]}]`)
}

func TestResumeCustomFlowUsesNeutralExecutor(t *testing.T) {
	executor := &fakeFlowExecutor{}
	resp, err := ResumeCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Body:    []byte(`{"interviewId":"301xx0000000001AAA"}`),
	}, flowActionDeps(executor, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ResumeCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if executor.resumeID != "301xx0000000001AAA" || executor.user.ID != testUser().ID {
		t.Fatalf("resume call = id %q user %#v", executor.resumeID, executor.user)
	}
	assertBody(t, "flow-resume-success", resp.Body, "", `{"success":true}`)
}

func TestResumeCustomFlowMapsDomainNotFound(t *testing.T) {
	resp, err := ResumeCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Body:    []byte(`{"interviewId":"missing"}`),
	}, flowActionDeps(&fakeFlowExecutor{resumeErr: sharedErrors.NewNotFoundWireError("FlowInterview not found")}, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ResumeCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, "FlowInterview not found"))
	assertBody(t, "flow-resume-not-found", resp.Body, want, want)
}

func TestResumeCustomFlowMapsHTTPStatusErrors(t *testing.T) {
	tests := []struct {
		name       string
		err        error
		wantStatus int
		wantCode   string
		wantMsg    string
	}{
		{
			name:       "forbidden",
			err:        flowHTTPStatusError{status: http.StatusForbidden, msg: "permission denied"},
			wantStatus: http.StatusForbidden,
			wantCode:   wire.SFCodeForbidden,
			wantMsg:    "permission denied",
		},
		{
			name:       "conflict",
			err:        flowHTTPStatusError{status: http.StatusConflict, msg: "could not acquire row lock"},
			wantStatus: http.StatusConflict,
			wantCode:   wire.SFCodeUnableToLockRow,
			wantMsg:    "could not acquire row lock",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			resp, err := ResumeCustomFlowNeutralHandler(context.Background(), &dialect.Request{
				Version: "v63.0",
				User:    testUser(),
				Body:    []byte(`{"interviewId":"301xx0000000001AAA"}`),
			}, flowActionDeps(&fakeFlowExecutor{resumeErr: tc.err}, &fakeFlowCatalog{}))
			if err != nil {
				t.Fatalf("ResumeCustomFlowNeutralHandler returned error: %v", err)
			}
			if resp.Status != tc.wantStatus {
				t.Fatalf("status = %d, want %d; body=%#v", resp.Status, tc.wantStatus, resp.Body)
			}
			want := mustJSON(t, wire.SFErrorResponse(tc.wantCode, tc.wantMsg))
			assertBody(t, "flow-resume-"+tc.name, resp.Body, want, want)
		})
	}
}

func TestFlowActionMissingExecutorIsSanitized(t *testing.T) {
	resp, err := ExecuteCustomFlowNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "DoThing"},
		Body:    []byte(`{"inputs":[{}]}`),
	}, flowActionDeps(nil, &fakeFlowCatalog{}))
	if err != nil {
		t.Fatalf("ExecuteCustomFlowNeutralHandler returned error: %v", err)
	}
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	want := mustJSON(t, wire.SFErrorResponse(wire.SFCodeInternalError, flowActionGenericInternalErrorMessage))
	assertBody(t, "flow-execute-missing-executor", resp.Body, want, want)
}

func flowActionDeps(executor contract.FlowExecutor, catalog contract.FlowCatalog) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{FlowExecutor: executor, FlowCatalog: catalog}}
}

type fakeFlowExecutor struct {
	called    bool
	flowName  string
	inputs    map[string]any
	resumeID  string
	user      *models.UserSession
	result    *contract.FlowExecutionResult
	err       error
	resumeErr error
}

func (f *fakeFlowExecutor) ExecuteFlow(_ context.Context, flowName string, inputs map[string]any, user *models.UserSession) (*contract.FlowExecutionResult, error) {
	f.called = true
	f.flowName = flowName
	f.inputs = inputs
	f.user = user
	if f.err != nil {
		return nil, f.err
	}
	return f.result, nil
}

func (f *fakeFlowExecutor) ResumeInterview(_ context.Context, interviewID string, user *models.UserSession) error {
	f.called = true
	f.resumeID = interviewID
	f.user = user
	return f.resumeErr
}

type fakeFlowCatalog struct {
	protected  []string
	flows      []contract.FlowDescriptor
	flow       *contract.FlowDescriptor
	version    *contract.FlowRevisionDescriptor
	schemaName string
	versionID  string
	listCalled bool
	err        error
}

func (f *fakeFlowCatalog) ProtectedFlowNames(context.Context) ([]string, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.protected, nil
}

func (f *fakeFlowCatalog) ListFlows(context.Context) ([]contract.FlowDescriptor, error) {
	if f.err != nil {
		return nil, f.err
	}
	f.listCalled = true
	return f.flows, nil
}

func (f *fakeFlowCatalog) GetFlow(_ context.Context, flowAPIName string) (*contract.FlowDescriptor, error) {
	if f.err != nil {
		return nil, f.err
	}
	f.schemaName = flowAPIName
	return f.flow, nil
}

func (f *fakeFlowCatalog) GetFlowRevision(_ context.Context, versionID string) (*contract.FlowRevisionDescriptor, error) {
	if f.err != nil {
		return nil, f.err
	}
	f.versionID = versionID
	return f.version, nil
}

type sensitiveFlowInternalError string

func (e sensitiveFlowInternalError) Error() string           { return string(e) }
func (e sensitiveFlowInternalError) InternalCondition() bool { return true }

var _ error = sensitiveFlowInternalError("")

type flowHTTPStatusError struct {
	status int
	msg    string
}

func (e flowHTTPStatusError) Error() string   { return e.msg }
func (e flowHTTPStatusError) HTTPStatus() int { return e.status }
