package dialectsfcrm

import (
	"context"
	"net/http"
	"reflect"
	"testing"

	"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 TestDescribeGlobalPublishesReadableHistoryCapableObjects(t *testing.T) {
	account := historyDescribeTestAccount()
	metadata := &fakeDescribeMetadata{objects: []models.ObjectMetadata{account, {APIName: "AccountHistory"}}}
	policy := historyDescribeTestPolicy()
	resp, err := DescribeGlobalNeutralHandler(context.Background(), &dialect.Request{Version: "v63.0"}, dialectDescribeDeps(metadata, policy))
	if err != nil || resp.Status != http.StatusOK {
		t.Fatalf("DescribeGlobalNeutralHandler = %#v, %v", resp, err)
	}
	body := resp.Body.(describeGlobalResult)
	if got := []string{body.SObjects[0].Name, body.SObjects[1].Name}; !reflect.DeepEqual(got, []string{"Account", "AccountHistory"}) {
		t.Fatalf("global objects = %#v", got)
	}
	if !body.SObjects[1].Queryable || body.SObjects[1].Createable || body.SObjects[1].Urls["describe"] != "/services/data/v63.0/sobjects/AccountHistory/describe" {
		t.Fatalf("history global shape = %#v", body.SObjects[1])
	}
}

func TestDescribeOpportunityPublishesPinnedFieldHistoryName(t *testing.T) {
	opportunity := models.ObjectMetadata{
		APIName: "Opportunity", Label: "Opportunity", PluralLabel: "Opportunities",
		Fields: []models.FieldMetadata{{APIName: "StageName", Label: "Stage", Type: constants.FieldTypeString, TrackHistory: true}},
	}
	metadata := &fakeDescribeMetadata{
		objects: []models.ObjectMetadata{opportunity, {APIName: opportunityFieldHistoryObject}},
		schemas: map[string]*models.ObjectMetadata{"Opportunity": &opportunity},
	}
	policy := &fakeDescribePolicy{access: map[string]*contract.EffectiveObjectAccess{
		"Opportunity": {
			Object: contract.ObjectAccess{Read: true},
			Fields: map[string]contract.FieldAccess{"StageName": {Readable: true}},
		},
	}}

	globalResp, err := DescribeGlobalNeutralHandler(context.Background(), &dialect.Request{Version: "v63.0"}, dialectDescribeDeps(metadata, policy))
	if err != nil || globalResp.Status != http.StatusOK {
		t.Fatalf("DescribeGlobalNeutralHandler = %#v, %v", globalResp, err)
	}
	global := globalResp.Body.(describeGlobalResult)
	if got := []string{global.SObjects[0].Name, global.SObjects[1].Name}; !reflect.DeepEqual(got, []string{"Opportunity", opportunityFieldHistoryObject}) {
		t.Fatalf("global objects = %#v; want pinned field-history name", got)
	}
	if global.SObjects[1].Label != "Opportunity Field History" {
		t.Fatalf("history label = %q; want Opportunity Field History", global.SObjects[1].Label)
	}

	objectResp, err := DescribeObjectNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0", Params: map[string]string{"sobject": opportunityFieldHistoryObject},
	}, dialectDescribeDeps(metadata, policy))
	if err != nil || objectResp.Status != http.StatusOK {
		t.Fatalf("DescribeObjectNeutralHandler = %#v, %v", objectResp, err)
	}
	object := objectResp.Body.(describeSObjectResult)
	if object.Name != opportunityFieldHistoryObject || object.Label != "Opportunity Field History" || object.Fields[1].Name != "OpportunityId" {
		t.Fatalf("history describe = %#v", object)
	}
}

func TestDescribeHistoryUsesParentReadAndTrackedFieldFLS(t *testing.T) {
	account := historyDescribeTestAccount()
	metadata := &fakeDescribeMetadata{schemas: map[string]*models.ObjectMetadata{"Account": &account}}
	policy := historyDescribeTestPolicy()
	resp, err := DescribeObjectNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0", Params: map[string]string{"sobject": "AccountHistory"},
	}, dialectDescribeDeps(metadata, policy))
	if err != nil || resp.Status != http.StatusOK {
		t.Fatalf("DescribeObjectNeutralHandler = %#v, %v", resp, err)
	}
	body := resp.Body.(describeSObjectResult)
	if body.Name != "AccountHistory" || len(body.Fields) != 8 || body.Fields[1].Name != "AccountId" || !body.Queryable || body.Createable {
		t.Fatalf("history describe = %#v", body)
	}
	field := body.Fields[2]
	wantFields := []interface{}{
		map[string]interface{}{"active": true, "defaultValue": false, "label": "Name", "value": "Name"},
		map[string]interface{}{"active": true, "defaultValue": false, "label": "Former__c", "value": "Former__c"},
	}
	if !reflect.DeepEqual(field.PicklistValues, wantFields) {
		t.Fatalf("Field picklist = %#v; want readable live fields including disabled history", field.PicklistValues)
	}
	if !body.Fields[3].Readable || !body.Fields[4].Readable {
		t.Fatalf("OldValue/NewValue must be readable when a tracked field is readable: %#v", body.Fields)
	}
	if !reflect.DeepEqual(policy.fullSeen, []string{"Account"}) {
		t.Fatalf("policy calls = %#v; want parent object", policy.fullSeen)
	}
}

func TestDescribeHistoryRetainsParentWhoseOnlyFieldIsDisabled(t *testing.T) {
	account := models.ObjectMetadata{
		APIName: "Account", Label: "Account", PluralLabel: "Accounts",
		Fields: []models.FieldMetadata{{APIName: "Name", Label: "Account Name", Type: constants.FieldTypeString, TrackHistory: false}},
	}
	metadata := &fakeDescribeMetadata{
		objects: []models.ObjectMetadata{account},
		schemas: map[string]*models.ObjectMetadata{"Account": &account},
	}
	policy := &fakeDescribePolicy{access: map[string]*contract.EffectiveObjectAccess{
		"Account": {
			Object: contract.ObjectAccess{Read: true},
			Fields: map[string]contract.FieldAccess{"Name": {Readable: true}},
		},
	}}

	globalResp, err := DescribeGlobalNeutralHandler(context.Background(), &dialect.Request{Version: "v63.0"}, dialectDescribeDeps(metadata, policy))
	if err != nil || globalResp.Status != http.StatusOK {
		t.Fatalf("DescribeGlobalNeutralHandler = %#v, %v", globalResp, err)
	}
	global := globalResp.Body.(describeGlobalResult)
	if got := []string{global.SObjects[0].Name, global.SObjects[1].Name}; !reflect.DeepEqual(got, []string{"Account", "AccountHistory"}) {
		t.Fatalf("global objects = %#v; want disabled field history retained", got)
	}

	objectResp, err := DescribeObjectNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0", Params: map[string]string{"sobject": "AccountHistory"},
	}, dialectDescribeDeps(metadata, policy))
	if err != nil || objectResp.Status != http.StatusOK {
		t.Fatalf("DescribeObjectNeutralHandler = %#v, %v", objectResp, err)
	}
	object := objectResp.Body.(describeSObjectResult)
	if got := object.Fields[2].PicklistValues; !reflect.DeepEqual(got, []interface{}{map[string]interface{}{"active": true, "defaultValue": false, "label": "Name", "value": "Name"}}) {
		t.Fatalf("Field picklist = %#v; want disabled field retained", got)
	}
}

func TestDescribeHistoryRejectsUnsupportedFieldBearingParent(t *testing.T) {
	task := models.ObjectMetadata{
		APIName: "Task", Label: "Task", PluralLabel: "Tasks",
		Fields: []models.FieldMetadata{{APIName: "Subject", Label: "Subject", Type: constants.FieldTypeString}},
	}
	metadata := &fakeDescribeMetadata{
		objects: []models.ObjectMetadata{task},
		schemas: map[string]*models.ObjectMetadata{"Task": &task},
	}
	policy := &fakeDescribePolicy{access: map[string]*contract.EffectiveObjectAccess{
		"Task": {
			Object: contract.ObjectAccess{Read: true},
			Fields: map[string]contract.FieldAccess{"Subject": {Readable: true}},
		},
	}}

	globalResp, err := DescribeGlobalNeutralHandler(context.Background(), &dialect.Request{Version: "v63.0"}, dialectDescribeDeps(metadata, policy))
	if err != nil || globalResp.Status != http.StatusOK {
		t.Fatalf("DescribeGlobalNeutralHandler = %#v, %v", globalResp, err)
	}
	global := globalResp.Body.(describeGlobalResult)
	if got := []string{global.SObjects[0].Name}; !reflect.DeepEqual(got, []string{"Task"}) {
		t.Fatalf("global objects = %#v; TaskHistory must not be published", got)
	}

	objectResp, err := DescribeObjectNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0", Params: map[string]string{"sobject": "TaskHistory"},
	}, dialectDescribeDeps(metadata, policy))
	if err != nil || objectResp.Status != http.StatusNotFound {
		t.Fatalf("DescribeObjectNeutralHandler = %#v, %v; want 404", objectResp, err)
	}
}

func TestDescribeHistoryRejectsUnreadableParent(t *testing.T) {
	account := historyDescribeTestAccount()
	metadata := &fakeDescribeMetadata{schemas: map[string]*models.ObjectMetadata{"Account": &account}}
	policy := &fakeDescribePolicy{access: map[string]*contract.EffectiveObjectAccess{"Account": {Object: contract.ObjectAccess{Read: false}}}}
	resp, _ := DescribeObjectNeutralHandler(context.Background(), &dialect.Request{Params: map[string]string{"sobject": "AccountHistory"}}, dialectDescribeDeps(metadata, policy))
	if resp.Status != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", resp.Status)
	}
}

func historyDescribeTestAccount() models.ObjectMetadata {
	return models.ObjectMetadata{
		APIName: "Account", Label: "Account", PluralLabel: "Accounts",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Account Name", Type: constants.FieldTypeString, TrackHistory: true},
			{APIName: "Secret__c", Label: "Secret", Type: constants.FieldTypeString, TrackHistory: true},
			{APIName: "Former__c", Label: "Former", Type: constants.FieldTypeString, TrackHistory: false},
		},
	}
}

func historyDescribeTestPolicy() *fakeDescribePolicy {
	return &fakeDescribePolicy{access: map[string]*contract.EffectiveObjectAccess{
		"Account": {
			Object: contract.ObjectAccess{Read: true},
			Fields: map[string]contract.FieldAccess{
				"Name":      {Readable: true},
				"Secret__c": {Readable: false},
				"Former__c": {Readable: true},
			},
		},
	}}
}
