package dialectsfcrm

import (
	"context"
	"strings"
	"testing"
	"time"

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

func TestFlowInterviewListProjectsSObjectEnvelope(t *testing.T) {
	created := time.Date(2026, 6, 12, 17, 0, 0, 0, time.UTC)
	interviews := &fakeFlowInterviews{
		list: []contract.FlowInterviewRecord{{
			ID:              "301000000000001AAA",
			FlowRevisionID:  "300000000000001AAA",
			InterviewLabel:  "Deal Approval",
			InterviewStatus: "Paused",
			CurrentElement:  "Wait_For_Manager",
			OwnerID:         "005000000000001AAA",
			CreatedDate:     created,
		}},
	}
	resp, err := ListFlowInterviewsNeutralHandler(context.Background(), &dialect.Request{
		User: &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(interviews, nil))
	if err != nil {
		t.Fatalf("ListFlowInterviewsNeutralHandler err = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "flow-interview-list", resp.Body, "",
		`{"done":true,"records":[{"Id":"301000000000001AAA","FlowVersionId":"300000000000001AAA","InterviewLabel":"Deal Approval","InterviewStatus":"Paused","CurrentElement":"Wait_For_Manager","OwnerId":"005000000000001AAA","CreatedDate":"2026-06-12T17:00:00Z"}],"totalSize":1}`)
}

func TestCreateFlowVersionRejectsVersionNumberBeforeContract(t *testing.T) {
	authoring := &fakeFlowAuthoring{}
	resp, err := CreateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
		Body: []byte(`{"DefinitionId":"300000000000001AAA","vErSiOnNuMbEr":99}`),
		User: &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("CreateFlowVersionNeutralHandler err = %v", err)
	}
	if resp.Status != 400 {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "flow-version-number-denied", resp.Body,
		mustJSON(t, wire.SFErrorWithFields(
			wire.SFCodeInvalidFieldForInsertUpdate,
			"Unable to create/update fields: VersionNumber. Please check the security settings of this field and verify that it is read/write for your profile or permission set.",
			"VersionNumber",
		)),
		`[{"errorCode":"INVALID_FIELD_FOR_INSERT_UPDATE","message":"Unable to create/update fields: VersionNumber. Please check the security settings of this field and verify that it is read/write for your profile or permission set.","fields":["VersionNumber"]}]`)
	if authoring.createFlowVersionCalled {
		t.Fatal("CreateFlowVersion contract was called after VersionNumber denial")
	}
}

func TestCreateFlowVersionUsesToolingAuthoringContract(t *testing.T) {
	authoring := &fakeFlowAuthoring{
		createFlowVersionResult: &contract.FlowVersionCreateResult{
			ID:            "301000000000001AAA",
			VersionNumber: 2,
		},
	}
	resp, err := CreateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
		Body: []byte(`{"Id":"301client","DefinitionId":"300def","MasterLabel":"New Flow","Status":"Draft","Description":"draft","ProcessType":"AutoLaunchedFlow","Metadata":{"label":"New Flow"}}`),
		User: &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("CreateFlowVersionNeutralHandler err = %v", err)
	}
	if resp.Status != 201 {
		t.Fatalf("status = %d, want 201; body=%#v", resp.Status, resp.Body)
	}
	if !authoring.createFlowVersionCalled {
		t.Fatal("CreateFlowVersion contract was not called")
	}
	got := authoring.createFlowVersionReq
	if got.ID != "301client" || got.DefinitionID != "300def" || got.MasterLabel != "New Flow" ||
		got.Status != "Draft" || got.Description != "draft" || got.ProcessType != "AutoLaunchedFlow" {
		t.Fatalf("create request = %+v", got)
	}
	if !strings.Contains(string(got.MetadataRaw), `"label":"New Flow"`) {
		t.Fatalf("metadata raw = %s, want original Metadata payload", string(got.MetadataRaw))
	}
	assertBody(t, "flow-version-create", resp.Body, "",
		`{"VersionNumber":2,"errors":[],"id":"301000000000001AAA","success":true}`)
}

func TestCreateFlowVersionDefaultsToolingProcessTypeAboveContract(t *testing.T) {
	authoring := &fakeFlowAuthoring{}
	resp, err := CreateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
		Body: []byte(`{"DefinitionId":"300def","MasterLabel":"New Flow","Status":"Draft","Metadata":{"label":"New Flow"}}`),
		User: &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("CreateFlowVersionNeutralHandler err = %v", err)
	}
	if resp.Status != 201 {
		t.Fatalf("status = %d, want 201; body=%#v", resp.Status, resp.Body)
	}
	if !authoring.createFlowVersionCalled {
		t.Fatal("CreateFlowVersion contract was not called")
	}
	if got := authoring.createFlowVersionReq.ProcessType; got != "AutoLaunchedFlow" {
		t.Fatalf("contract ProcessType = %q, want AutoLaunchedFlow", got)
	}
}

func TestCreateFlowDefinitionMalformedJSONKeepsLegacyInvalidRequestEnvelope(t *testing.T) {
	authoring := &fakeFlowAuthoring{}
	resp, err := CreateFlowDefinitionNeutralHandler(context.Background(), &dialect.Request{
		Body: []byte(`{"DeveloperName":`),
		User: &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("CreateFlowDefinitionNeutralHandler err = %v", err)
	}
	if resp.Status != 400 {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "flow-definition-malformed-json", resp.Body, "",
		`{"code":"InvalidRequest","message":"unexpected EOF"}`)
	if authoring.createFlowDefinitionCalled {
		t.Fatal("CreateFlowDefinition contract was called after malformed JSON")
	}
}

func TestFlowGetMissingKeepsLegacyNotFoundEnvelope(t *testing.T) {
	cases := []struct {
		name string
		run  func() (*dialect.Response, error)
		want string
	}{
		{
			name: "flow interview",
			run: func() (*dialect.Response, error) {
				return GetFlowInterviewNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(&fakeFlowInterviews{}, nil))
			},
			want: `{"code":"ResourceNotFound","message":"FlowInterview"}`,
		},
		{
			name: "flow definition",
			run: func() (*dialect.Response, error) {
				return GetFlowDefinitionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "300-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, &fakeFlowAuthoring{}))
			},
			want: `{"code":"ResourceNotFound","message":"FlowDefinition"}`,
		},
		{
			name: "flow version",
			run: func() (*dialect.Response, error) {
				return GetFlowVersionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, &fakeFlowAuthoring{}))
			},
			want: `{"code":"ResourceNotFound","message":"Flow"}`,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			resp, err := tc.run()
			if err != nil {
				t.Fatalf("handler err = %v", err)
			}
			if resp.Status != 404 {
				t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
			}
			assertBody(t, tc.name, resp.Body, "", tc.want)
		})
	}
}

func TestFlowToolingWriteNotFoundProjectsV63NotFound(t *testing.T) {
	const message = "Flow with ID 'missing' not found"
	missing := flowToolingNotFoundError{message: message}
	cases := []struct {
		name string
		run  func(*fakeFlowAuthoring) (*dialect.Response, error)
		set  func(*fakeFlowAuthoring)
	}{
		{
			name: "patch flow definition",
			set:  func(f *fakeFlowAuthoring) { f.patchFlowDefinitionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return UpdateFlowDefinitionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "300-missing"},
					Body:   []byte(`{"Description":"after"}`),
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
		{
			name: "delete flow definition",
			set:  func(f *fakeFlowAuthoring) { f.deleteFlowDefinitionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return DeleteFlowDefinitionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "300-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
		{
			name: "patch flow version",
			set:  func(f *fakeFlowAuthoring) { f.patchFlowVersionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return UpdateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					Body:   []byte(`{"Description":"after"}`),
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
		{
			name: "delete flow version",
			set:  func(f *fakeFlowAuthoring) { f.deleteFlowVersionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return DeleteFlowVersionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
		{
			name: "activate flow version",
			set:  func(f *fakeFlowAuthoring) { f.activateFlowVersionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return ActivateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
		{
			name: "deactivate flow version",
			set:  func(f *fakeFlowAuthoring) { f.deactivateFlowVersionErr = missing },
			run: func(f *fakeFlowAuthoring) (*dialect.Response, error) {
				return DeactivateFlowVersionNeutralHandler(context.Background(), &dialect.Request{
					Params: map[string]string{"id": "301-missing"},
					User:   &models.UserSession{ID: "005000000000001AAA"},
				}, flowDeps(nil, f))
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			authoring := &fakeFlowAuthoring{}
			tc.set(authoring)
			resp, err := tc.run(authoring)
			if err != nil {
				t.Fatalf("handler err = %v", err)
			}
			if resp.Status != 404 {
				t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
			}
			assertBody(t, tc.name, resp.Body, "",
				`[{"errorCode":"NOT_FOUND","message":"Flow with ID 'missing' not found"}]`)
		})
	}
}

func TestFlowVersionListPreservesNilMetadataAsNull(t *testing.T) {
	authoring := &fakeFlowAuthoring{
		listFlowVersionsResult: []contract.FlowVersionRecord{{
			ID:            "301000000000001AAA",
			DefinitionID:  "300000000000001AAA",
			VersionNumber: 1,
			Status:        "Draft",
			ProcessType:   "AutoLaunchedFlow",
			MetadataRaw:   nil,
		}},
	}
	resp, err := ListFlowVersionsNeutralHandler(context.Background(), &dialect.Request{
		Query: map[string][]string{"DefinitionId": []string{"300000000000001AAA"}},
		User:  &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("ListFlowVersionsNeutralHandler err = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if authoring.listFlowVersionsDefinitionID != "300000000000001AAA" {
		t.Fatalf("DefinitionId = %q, want 300000000000001AAA", authoring.listFlowVersionsDefinitionID)
	}
	assertBody(t, "flow-version-list-null-metadata", resp.Body, "",
		`{"done":true,"records":[{"Id":"301000000000001AAA","DefinitionId":"300000000000001AAA","VersionNumber":1,"Status":"Draft","ProcessType":"AutoLaunchedFlow","Metadata":null}],"totalSize":1}`)
}

func TestFlowVersionGetPreservesEmptyMetadataAsNull(t *testing.T) {
	authoring := &fakeFlowAuthoring{
		getFlowVersionResult: &contract.FlowVersionRecord{
			ID:            "301000000000001AAA",
			DefinitionID:  "300000000000001AAA",
			VersionNumber: 1,
			Status:        "Draft",
			ProcessType:   "AutoLaunchedFlow",
			MetadataRaw:   []byte{},
		},
	}
	resp, err := GetFlowVersionNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "301000000000001AAA"},
		User:   &models.UserSession{ID: "005000000000001AAA"},
	}, flowDeps(nil, authoring))
	if err != nil {
		t.Fatalf("GetFlowVersionNeutralHandler err = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if authoring.getFlowVersionID != "301000000000001AAA" {
		t.Fatalf("id = %q, want 301000000000001AAA", authoring.getFlowVersionID)
	}
	assertBody(t, "flow-version-get-null-metadata", resp.Body, "",
		`{"Id":"301000000000001AAA","DefinitionId":"300000000000001AAA","VersionNumber":1,"Status":"Draft","ProcessType":"AutoLaunchedFlow","Metadata":null}`)
}

func TestFlowAuthoringErrorProjection(t *testing.T) {
	req := &dialect.Request{Version: "v63.0"}
	cases := []struct {
		name string
		err  error
		want string
	}{
		{
			name: "fls",
			err: &contract.FlowAuthoringError{
				Kind:    contract.FlowAuthoringErrorFieldDenied,
				Message: "Unable to create/update fields: TriggerType.",
				Fields:  []string{"TriggerType"},
			},
			want: `[{"errorCode":"INVALID_FIELD_FOR_INSERT_UPDATE","message":"Unable to create/update fields: TriggerType.","fields":["TriggerType"]}]`,
		},
		{
			name: "duplicate version",
			err:  &contract.FlowAuthoringError{Kind: contract.FlowAuthoringErrorDuplicateVersion},
			want: `[{"errorCode":"DUPLICATE_VALUE","message":"A conflicting flow version already exists."}]`,
		},
		{
			name: "auto developer duplicate",
			err: &contract.FlowAuthoringError{
				Kind:          contract.FlowAuthoringErrorDuplicateAutoCreatedDeveloper,
				DeveloperName: "New_Flow_123",
			},
			want: `[{"errorCode":"DUPLICATE_DEVELOPER_NAME","message":"A flow with DeveloperName 'New_Flow_123' already exists. Please use a different name."}]`,
		},
		{
			name: "lock",
			err:  &contract.FlowAuthoringError{Kind: contract.FlowAuthoringErrorLockContention},
			want: `[{"errorCode":"UNABLE_TO_LOCK_ROW","message":"The flow definition is locked by a concurrent change. Retry the request."}]`,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			resp := flowToolingErrorResponse(req, "test", tc.err)
			if resp == nil {
				t.Fatal("response = nil")
			}
			assertBody(t, tc.name, resp.Body, "", tc.want)
		})
	}
}

func TestFlowAuthoringErrorProjectionKeepsInternalOpaque(t *testing.T) {
	resp := flowToolingErrorResponse(&dialect.Request{Version: "v63.0"}, "test",
		&contract.FlowAuthoringError{
			Kind:  contract.FlowAuthoringErrorFieldDenied,
			Cause: internalFlowToolingError{},
		},
	)
	if resp == nil {
		t.Fatal("response = nil")
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500", resp.Status)
	}
	assertBody(t, "internal-first", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
}

func flowDeps(interviews contract.FlowInterviews, authoring contract.FlowAuthoring) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{FlowInterviews: interviews, FlowAuthoring: authoring}}
}

type internalFlowToolingError struct{}

func (internalFlowToolingError) Error() string {
	return "internal flow tooling failure"
}

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

type flowToolingNotFoundError struct {
	message string
}

func (e flowToolingNotFoundError) Error() string {
	return e.message
}

func (e flowToolingNotFoundError) NotFoundCondition() bool {
	return true
}

type fakeFlowInterviews struct {
	list []contract.FlowInterviewRecord
}

func (f *fakeFlowInterviews) ListFlowInterviews(context.Context, *models.UserSession) ([]contract.FlowInterviewRecord, error) {
	return append([]contract.FlowInterviewRecord(nil), f.list...), nil
}

func (f *fakeFlowInterviews) GetFlowInterview(context.Context, string, *models.UserSession) (*contract.FlowInterviewRecord, error) {
	return nil, nil
}

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

type fakeFlowAuthoring struct {
	createFlowDefinitionCalled   bool
	createFlowVersionCalled      bool
	createFlowVersionReq         contract.FlowVersionWrite
	createFlowVersionResult      *contract.FlowVersionCreateResult
	listFlowVersionsDefinitionID string
	listFlowVersionsResult       []contract.FlowVersionRecord
	getFlowVersionID             string
	getFlowVersionResult         *contract.FlowVersionRecord
	patchFlowDefinitionIntent    contract.FlowAdminDefinitionPatchIntent
	patchFlowDefinitionErr       error
	deleteFlowDefinitionErr      error
	patchFlowVersionIntent       contract.FlowAdminVersionPatchIntent
	patchFlowVersionErr          error
	deleteFlowVersionErr         error
	activateFlowVersionErr       error
	deactivateFlowVersionErr     error
}

func (f *fakeFlowAuthoring) ListFlowDefinitions(context.Context) ([]contract.FlowDefinitionRecord, error) {
	return nil, nil
}

func (f *fakeFlowAuthoring) GetFlowDefinition(context.Context, string) (*contract.FlowDefinitionRecord, error) {
	return nil, nil
}

func (f *fakeFlowAuthoring) CreateFlowDefinition(context.Context, contract.FlowDefinitionWrite) (*contract.FlowDefinitionCreateResult, error) {
	f.createFlowDefinitionCalled = true
	return nil, nil
}

func (f *fakeFlowAuthoring) PatchFlowDefinition(_ context.Context, _ string, intent contract.FlowAdminDefinitionPatchIntent) error {
	f.patchFlowDefinitionIntent = intent
	return f.patchFlowDefinitionErr
}

func (f *fakeFlowAuthoring) DeleteFlowDefinition(context.Context, string) error {
	return f.deleteFlowDefinitionErr
}

func (f *fakeFlowAuthoring) ListFlowVersions(_ context.Context, definitionID string) ([]contract.FlowVersionRecord, error) {
	f.listFlowVersionsDefinitionID = definitionID
	return append([]contract.FlowVersionRecord(nil), f.listFlowVersionsResult...), nil
}

func (f *fakeFlowAuthoring) GetFlowVersion(_ context.Context, id string) (*contract.FlowVersionRecord, error) {
	f.getFlowVersionID = id
	if f.getFlowVersionResult == nil {
		return nil, nil
	}
	result := *f.getFlowVersionResult
	return &result, nil
}

func (f *fakeFlowAuthoring) CreateFlowVersion(_ context.Context, req contract.FlowVersionWrite) (*contract.FlowVersionCreateResult, error) {
	f.createFlowVersionCalled = true
	f.createFlowVersionReq = req
	if f.createFlowVersionResult != nil {
		return f.createFlowVersionResult, nil
	}
	return &contract.FlowVersionCreateResult{}, nil
}

func (f *fakeFlowAuthoring) PatchFlowVersion(_ context.Context, _ string, intent contract.FlowAdminVersionPatchIntent) error {
	f.patchFlowVersionIntent = intent
	return f.patchFlowVersionErr
}

func (f *fakeFlowAuthoring) DeleteFlowVersion(context.Context, string) error {
	return f.deleteFlowVersionErr
}

func (f *fakeFlowAuthoring) ActivateFlowVersion(context.Context, string) error {
	return f.activateFlowVersionErr
}

func (f *fakeFlowAuthoring) DeactivateFlowVersion(context.Context, string) error {
	return f.deactivateFlowVersionErr
}
