package sfcrmappservice

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	metadataadminsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/metadataadminreadv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
)

type metadataAdminReadPorts struct {
	indexCalls  int
	ruleCalls   int
	index       metadataadminsemantic.ObjectAdminIndexResponse
	rules       metadataadminsemantic.ValidationRuleAdminIndexResponse
	ruleRequest metadataadminsemantic.ValidationRuleAdminIndexRequest
	err         error
}

func (ports *metadataAdminReadPorts) readIndex(
	context.Context, string, metadataadminsemantic.ObjectAdminIndexRequest,
) (metadataadminsemantic.ObjectAdminIndexResponse, error) {
	ports.indexCalls++
	return ports.index, ports.err
}

func (ports *metadataAdminReadPorts) readRules(
	_ context.Context, _ string, request metadataadminsemantic.ValidationRuleAdminIndexRequest,
) (metadataadminsemantic.ValidationRuleAdminIndexResponse, error) {
	ports.ruleCalls++
	ports.ruleRequest = request
	return ports.rules, ports.err
}

func TestMetadataAdminReadRoutesProjectCurrentAuthoringDTOs(t *testing.T) {
	description := "Customer accounts"
	receipts := metadataAdminReadTestReceipts(17, 3)
	field := "Amount"
	ownerPack := "salesforce-crm-standard"
	ports := &metadataAdminReadPorts{
		index: metadataadminsemantic.ObjectAdminIndexResponse{
			Objects: []metadataadminsemantic.ObjectAdminMetadata{
				{ObjectID: "Account", Description: &description, DeleteMode: "soft"},
				{ObjectID: "Contact"},
			},
			Receipts: receipts,
		},
		rules: metadataadminsemantic.ValidationRuleAdminIndexResponse{
			ObjectID: "Opportunity",
			Rules: []metadataadminsemantic.ValidationRuleMetadata{{
				RuleID: "rule-a", ObjectID: "Opportunity", Name: "PositiveAmount",
				Description: &description, Active: true, Condition: "Amount < 0",
				ErrorMessage: "Amount must be positive", ErrorDisplayField: &field,
				IsCustomized: true, OwnerPack: &ownerPack,
				RecordSetBindings: []metadataadminsemantic.ValidationRuleRecordSetBinding{{
					Name: "hasOpenQuote", Mode: "exists", ObjectID: "Quote", FilterLogic: "1",
					Filters: []metadataadminsemantic.ValidationRuleRecordSetFilter{{
						Field: "Status", Operator: "eq", ValueSource: "literal", Value: json.RawMessage(`"Open"`),
					}},
				}},
			}},
			Receipts: receipts,
		},
	}
	service := metadataAdminReadTestService(t, ports)

	tests := []struct {
		path   string
		assert func(*testing.T, []byte)
	}{
		{path: "/services/data/v63.0/metadata/objects", assert: func(t *testing.T, body []byte) {
			var result struct {
				Objects []struct {
					Name, DeleteMode string
					Description      *string
				} `json:"sobjects"`
			}
			if err := json.Unmarshal(body, &result); err != nil || len(result.Objects) != 2 ||
				result.Objects[0].Name != "Account" || result.Objects[0].Description == nil ||
				*result.Objects[0].Description != description || result.Objects[1].Description != nil {
				t.Fatalf("object index = %#v, error=%v", result, err)
			}
		}},
		{path: "/services/data/v63.0/metadata/objects/account", assert: func(t *testing.T, body []byte) {
			var result struct {
				Name, DeleteMode string
				Description      *string
			}
			if err := json.Unmarshal(body, &result); err != nil || result.Name != "Account" ||
				result.Description == nil || *result.Description != description {
				t.Fatalf("object detail = %#v, error=%v", result, err)
			}
		}},
		{path: "/services/data/v63.0/metadata/validation-rules?objectApiName=Opportunity", assert: func(t *testing.T, body []byte) {
			var result struct {
				Rules []struct {
					ID, ObjectAPIName, Name, Condition, ErrorMessage string
					Active                                           bool
					Description                                      *string
					ErrorDisplayField                                *string
					IsCustomized                                     bool
					OwnerPack                                        *string
					RecordSetBindings                                []struct {
						ObjectAPIName string
						Filters       []struct{ Value any }
					}
				} `json:"rules"`
			}
			if err := json.Unmarshal(body, &result); err != nil || len(result.Rules) != 1 ||
				result.Rules[0].ID != "rule-a" || result.Rules[0].ObjectAPIName != "Opportunity" ||
				!result.Rules[0].Active || result.Rules[0].Description == nil || *result.Rules[0].Description != description ||
				result.Rules[0].ErrorDisplayField == nil ||
				*result.Rules[0].ErrorDisplayField != field || len(result.Rules[0].RecordSetBindings) != 1 ||
				!result.Rules[0].IsCustomized || result.Rules[0].OwnerPack == nil || *result.Rules[0].OwnerPack != ownerPack ||
				result.Rules[0].RecordSetBindings[0].ObjectAPIName != "Quote" ||
				len(result.Rules[0].RecordSetBindings[0].Filters) != 1 ||
				result.Rules[0].RecordSetBindings[0].Filters[0].Value != "Open" {
				t.Fatalf("validation rules = %#v, error=%v", result, err)
			}
		}},
	}
	for _, test := range tests {
		t.Run(test.path, func(t *testing.T) {
			request := httptest.NewRequest(http.MethodGet, test.path, nil)
			request.Header.Set("Authorization", "Bearer end-user")
			response := httptest.NewRecorder()
			service.ServeHTTP(response, request)
			if response.Code != http.StatusOK {
				t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
			}
			test.assert(t, response.Body.Bytes())
		})
	}
	if ports.indexCalls != 2 || ports.ruleCalls != 1 ||
		ports.ruleRequest.ObjectID != "Opportunity" {
		t.Fatalf("Runtime index/rules calls = %d/%d request=%#v", ports.indexCalls,
			ports.ruleCalls, ports.ruleRequest)
	}
}

func TestMetadataAdminReadMatchesValidationRuleCollectionRoute(t *testing.T) {
	route, matched := matchMetadataAdminRoute("/services/data/v63.0/metadata/validation-rules")
	if !matched || route.kind != metadataAdminValidationRuleIndex {
		t.Fatalf("validation-rule route = %#v, matched=%t", route, matched)
	}
}

func TestMetadataAdminReadRoutesExposeFailureInsteadOfEmptySuccess(t *testing.T) {
	for _, path := range []string{
		"/services/data/v63.0/metadata/objects",
		"/services/data/v63.0/metadata/validation-rules?objectApiName=Account",
	} {
		t.Run(path, func(t *testing.T) {
			ports := &metadataAdminReadPorts{
				err: &metadataadminsemantic.Failure{Category: metadataadminsemantic.FailureUnavailable},
			}
			request := httptest.NewRequest(http.MethodGet, path, nil)
			request.Header.Set("Authorization", "Bearer end-user")
			response := httptest.NewRecorder()
			metadataAdminReadTestService(t, ports).ServeHTTP(response, request)
			if response.Code != http.StatusServiceUnavailable ||
				response.Body.String() == "" || response.Body.String() == "[]\n" ||
				response.Body.String() == `{"layouts":[],"totalSize":0}`+"\n" {
				t.Fatalf("status=%d body=%q", response.Code, response.Body.String())
			}
		})
	}
}

func TestMetadataAdminReadRoutesRejectMissingBearerBeforeRuntime(t *testing.T) {
	for _, path := range []string{
		"/services/data/v63.0/metadata/objects",
		"/services/data/v63.0/metadata/validation-rules?objectApiName=Opportunity",
	} {
		t.Run(path, func(t *testing.T) {
			ports := &metadataAdminReadPorts{}
			request := httptest.NewRequest(http.MethodGet, path, nil)
			response := httptest.NewRecorder()
			metadataAdminReadTestService(t, ports).ServeHTTP(response, request)
			if response.Code != http.StatusUnauthorized || ports.indexCalls != 0 || ports.ruleCalls != 0 {
				t.Fatalf("status=%d index/rule calls=%d/%d body=%s", response.Code,
					ports.indexCalls, ports.ruleCalls, response.Body.String())
			}
		})
	}
}

func TestMetadataAdminReadValidationRuleRouteValidatesObjectQueryBeforeRuntime(t *testing.T) {
	for _, test := range []struct {
		name string
		path string
	}{
		{name: "missing", path: "/services/data/v63.0/metadata/validation-rules"},
		{name: "invalid", path: "/services/data/v63.0/metadata/validation-rules?objectApiName=%20Opportunity"},
	} {
		t.Run(test.name, func(t *testing.T) {
			ports := &metadataAdminReadPorts{}
			request := httptest.NewRequest(http.MethodGet, test.path, nil)
			request.Header.Set("Authorization", "Bearer end-user")
			response := httptest.NewRecorder()
			metadataAdminReadTestService(t, ports).ServeHTTP(response, request)
			if response.Code != http.StatusBadRequest || ports.ruleCalls != 0 {
				t.Fatalf("status=%d calls=%d body=%s", response.Code, ports.ruleCalls, response.Body.String())
			}
			var failure []struct {
				ErrorCode string   `json:"errorCode"`
				Fields    []string `json:"fields"`
			}
			if err := json.Unmarshal(response.Body.Bytes(), &failure); err != nil || len(failure) != 1 ||
				failure[0].ErrorCode != "INVALID_FIELD" || len(failure[0].Fields) != 1 || failure[0].Fields[0] != "objectApiName" {
				t.Fatalf("failure=%#v error=%v body=%s", failure, err, response.Body.String())
			}
		})
	}
}

func TestMetadataAdminReadOwnershipDoesNotCaptureMutationMethods(t *testing.T) {
	ports := &metadataAdminReadPorts{}
	service := metadataAdminReadTestService(t, ports)
	tests := []struct {
		method string
		path   string
	}{
		{method: http.MethodPost, path: "/services/data/v63.0/metadata/objects"},
		{method: http.MethodPatch, path: "/services/data/v63.0/metadata/objects/Account"},
		{method: http.MethodDelete, path: "/services/data/v63.0/metadata/page-layouts/layout-a"},
		{method: http.MethodPost, path: "/services/data/v63.0/metadata/validation-rules"},
	}
	for _, test := range tests {
		t.Run(test.method+" "+test.path, func(t *testing.T) {
			recorder := httptest.NewRecorder()
			request := httptest.NewRequest(test.method, test.path, nil)
			if service.serveMetadataAdminRead(recorder, request) {
				t.Fatal("metadata administration read capability captured a mutation")
			}
		})
	}
	if ports.indexCalls != 0 || ports.ruleCalls != 0 {
		t.Fatalf("mutation reached Runtime read ports: %#v", ports)
	}
}

func metadataAdminReadTestService(t *testing.T, ports *metadataAdminReadPorts) *Service {
	t.Helper()
	return metadataAdminReadService(t, ports.readIndex, ports.readRules)
}

func metadataAdminReadService(
	t *testing.T,
	readIndex RuntimeReadObjectAdminIndexFunc,
	readRules RuntimeReadValidationRuleAdminIndexFunc,
) *Service {
	t.Helper()
	runtime := &fakeRuntime{}
	runtime.ready.Store(true)
	config := ServiceConfig{
		RuntimeEvaluate: runtime.Evaluate, RuntimeReady: runtime.Ready, RuntimeMetrics: runtime.Metrics,
		RuntimeQuery: func(context.Context, string, querysemantic.Request) (querysemantic.Response, error) {
			return querysemantic.Response{}, nil
		},
		RuntimeHistoryQuery: func(context.Context, string, querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
			return querysemantic.HistoryResponse{}, nil
		},
		RuntimeReadObjectAdminIndex:         readIndex,
		RuntimeReadValidationRuleAdminIndex: readRules,
		RequestTimeout:                      time.Second, MaxRequestBytes: 64 * 1024,
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	return service
}

func metadataAdminReadTestReceipts(revision, securityEpoch uint64) metadataadminsemantic.Receipts {
	return metadataadminsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: metadataadminsemantic.CapabilityName,
		CatalogRevision: revision, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: securityEpoch,
	}
}
