package records

import (
	"context"
	stderrors "errors"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestContractRecordToSObjectCopiesCarrier(t *testing.T) {
	source := contract.Record{"Id": "001"}
	got := contractRecordToSObject(source)
	if got["Id"] != "001" {
		t.Fatalf("contractRecordToSObject() = %#v", got)
	}
	source["Id"] = "mutated"
	if got["Id"] != "001" {
		t.Fatalf("SObject aliased neutral record: %#v", got)
	}
	if _, ok := interface{}(got).(models.SObject); !ok {
		t.Fatalf("type = %T, want models.SObject", got)
	}
}

func TestContractRecordFromSObjectCopiesCarrier(t *testing.T) {
	source := models.SObject{
		"Id":          "001",
		"Probability": float64(42.5),
		"attributes":  map[string]interface{}{"type": "Account"},
	}
	schemautil.SetExactDecimalForField(source, "Probability", schemautil.ExactDecimal("42.500000000000000001"))
	got := contractRecordFromSObject(source)
	if got["Id"] != "001" {
		t.Fatalf("contractRecordFromSObject() = %#v", got)
	}
	if got[models.ObjectTypeMetadataKey] != "Account" {
		t.Fatalf("neutral object type = %#v, want Account", got[models.ObjectTypeMetadataKey])
	}
	if _, ok := got["attributes"]; ok {
		t.Fatalf("v63 attributes leaked into neutral record: %#v", got)
	}
	if _, ok := got[models.ExactDecimalMetadataKey]; ok {
		t.Fatalf("exact decimal metadata leaked into neutral record: %#v", got)
	}
	if probability, ok := got["Probability"].(float64); !ok || probability != 42.5 {
		t.Fatalf("legacy decimal value = %#v (%T), want float64 42.5", got["Probability"], got["Probability"])
	}
	source["Id"] = "mutated"
	if got["Id"] != "001" {
		t.Fatalf("neutral record aliased SObject: %#v", got)
	}
	if _, ok := interface{}(got).(contract.Record); !ok {
		t.Fatalf("type = %T, want contract.Record", got)
	}
}

func TestContractCarrierConversionsPreserveNil(t *testing.T) {
	if got := contractRecordToSObject(nil); got != nil {
		t.Fatalf("contractRecordToSObject(nil) = %#v, want nil", got)
	}
	if got := contractRecordFromSObject(nil); got != nil {
		t.Fatalf("contractRecordFromSObject(nil) = %#v, want nil", got)
	}
	if got := contractRecordsToSObjects(nil); got != nil {
		t.Fatalf("contractRecordsToSObjects(nil) = %#v, want nil", got)
	}
	if got := contractRecordsFromSObjects(nil); got != nil {
		t.Fatalf("contractRecordsFromSObjects(nil) = %#v, want nil", got)
	}
	if got := contractRecordMapFromSObjects(nil); got != nil {
		t.Fatalf("contractRecordMapFromSObjects(nil) = %#v, want nil", got)
	}
}

func TestContractServiceDoesNotExposeMetadataDeployDeleteCapability(t *testing.T) {
	ordinaryRecords := NewContractService(&PersistenceService{})
	if _, ok := interface{}(ordinaryRecords).(contract.MetadataDeployRecordDeleter); ok {
		t.Fatal("ordinary records adapter exposes metadata-deploy hard-delete capability")
	}

	deleter := NewMetadataDeployRecordDeleter(&PersistenceService{})
	if deleter == nil {
		t.Fatal("dedicated metadata-deploy delete adapter is nil")
	}
	if _, ok := interface{}(deleter).(contract.Records); ok {
		t.Fatal("metadata-deploy delete adapter exposes ordinary records surface")
	}
}

func TestContractServiceListViewGenericCollectionSurfacesFailClosed(t *testing.T) {
	svc := NewContractService(&PersistenceService{})
	user := &models.UserSession{ID: "005-owner"}
	listViewRecord := contract.Record{
		models.ObjectTypeMetadataKey:             constants.TableListView,
		constants.FieldSysListView_ObjectAPIName: "Account",
		constants.FieldSysListView_Label:         "My Accounts",
	}

	createResults, err := svc.CreateRecords(context.Background(), []contract.Record{listViewRecord}, false, user)
	if err != nil {
		t.Fatalf("CreateRecords(_System_ListView) error = %T %v, want per-record invalid operation result", err, err)
	}
	assertListViewInvalidOperationResult(t, "CreateRecords(_System_ListView)", createResults, "")

	updateResults, err := svc.UpdateRecords(context.Background(), []contract.Record{{
		models.ObjectTypeMetadataKey:       constants.TableListView,
		constants.FieldID:                  "lv-1",
		constants.FieldSysListView_Label:   "Renamed",
		constants.FieldSysListView_Fields:  []string{"Name"},
		constants.FieldSysListView_OwnerID: "005-owner",
	}}, false, user)
	if err != nil {
		t.Fatalf("UpdateRecords(_System_ListView) error = %T %v, want per-record invalid operation result", err, err)
	}
	assertListViewInvalidOperationResult(t, "UpdateRecords(_System_ListView)", updateResults, "lv-1")

	if _, err := svc.UpsertRecords(context.Background(), constants.TableListView, constants.FieldSysListView_DeveloperName, []contract.Record{listViewRecord}, false, user); !isListViewInvalidOperation(err) {
		t.Fatalf("UpsertRecords(_System_ListView) error = %T %v, want invalid operation", err, err)
	}
}

// TestContractServiceCreateTreeNodesRejectsListViewRegardlessOfBypass proves the
// adapter-level list-view guard on the neutral CreateTreeNodes surface fires
// UNCONDITIONALLY — in a normal context AND under a security-context bypass
// (SystemContext / ElevatedContext). The engine guard in
// PersistenceService.CreateTreeNodes is skipped under BypassesCRUDandFLS, so
// without the adapter guard a privileged caller could create _System_ListView
// rows through the generic tree surface (the regression this test locks in).
// The PersistenceService is empty, so the guard must fail closed before any DB
// access — a passing delegate would nil-deref rather than return this error.
func TestContractServiceCreateTreeNodesRejectsListViewRegardlessOfBypass(t *testing.T) {
	svc := NewContractService(&PersistenceService{})
	user := &models.UserSession{ID: "005-owner"}
	nodes := []contract.TreeNode{{
		ObjectType: constants.TableListView,
		Fields: contract.Record{
			constants.FieldSysListView_ObjectAPIName: "Account",
			constants.FieldSysListView_Label:         "My Accounts",
		},
	}}

	cases := []struct {
		name string
		ctx  context.Context
	}{
		{name: "normal", ctx: context.Background()},
		{name: "system-bypass", ctx: constants.WithSecurityContext(context.Background(), constants.SystemContext, "test")},
		{name: "elevated-bypass", ctx: constants.WithSecurityContext(context.Background(), constants.ElevatedContext, "test")},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			results, err := svc.CreateTreeNodes(tc.ctx, nodes, user)
			if !isListViewInvalidOperation(err) {
				t.Fatalf("CreateTreeNodes(_System_ListView) error = %T %v, want list-view invalid operation", err, err)
			}
			if results != nil {
				t.Fatalf("CreateTreeNodes(_System_ListView) results = %#v, want nil (nothing inserted)", results)
			}
		})
	}
}

// TestContractServiceCreateTreeNodesRejectsNestedListViewRegardlessOfBypass is the
// nested-closure counterpart: a _System_ListView node reached only as a child of
// an otherwise-legal root must still be rejected by the adapter guard under a
// bypass context.
func TestContractServiceCreateTreeNodesRejectsNestedListViewRegardlessOfBypass(t *testing.T) {
	svc := NewContractService(&PersistenceService{})
	user := &models.UserSession{ID: "005-owner"}
	nodes := []contract.TreeNode{{
		ObjectType: "Account",
		Fields:     contract.Record{"Name": "root"},
		Children: map[string][]contract.TreeNode{
			"Contacts": {{
				ObjectType: constants.TableListView,
				Fields: contract.Record{
					constants.FieldSysListView_ObjectAPIName: "Account",
					constants.FieldSysListView_Label:         "Nested",
				},
			}},
		},
	}}

	bypassCtx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "test")
	results, err := svc.CreateTreeNodes(bypassCtx, nodes, user)
	if !isListViewInvalidOperation(err) {
		t.Fatalf("CreateTreeNodes(nested _System_ListView) error = %T %v, want list-view invalid operation", err, err)
	}
	if results != nil {
		t.Fatalf("CreateTreeNodes(nested _System_ListView) results = %#v, want nil (nothing inserted)", results)
	}
}

func assertListViewInvalidOperationResult(t *testing.T, operation string, results []contract.RecordMutationResult, wantID string) {
	t.Helper()
	if len(results) != 1 {
		t.Fatalf("%s results = %#v, want one per-record result", operation, results)
	}
	assertListViewInvalidOperationEntry(t, operation, results[0], wantID)
}

func assertListViewInvalidOperationEntry(t *testing.T, operation string, got contract.RecordMutationResult, wantID string) {
	t.Helper()
	if got.Success {
		t.Fatalf("%s result success = true, want false", operation)
	}
	if got.ID != wantID {
		t.Fatalf("%s result ID = %q, want %q", operation, got.ID, wantID)
	}
	if len(got.Errors) != 1 {
		t.Fatalf("%s errors = %#v, want one error", operation, got.Errors)
	}
	if got.Errors[0].Kind != contract.RecordMutationErrorInvalidOperation {
		t.Fatalf("%s error kind = %q, want %q", operation, got.Errors[0].Kind, contract.RecordMutationErrorInvalidOperation)
	}
	if !strings.Contains(got.Errors[0].Message, constants.TableListView) {
		t.Fatalf("%s error message = %q, want %s", operation, got.Errors[0].Message, constants.TableListView)
	}
}

func TestContractServiceListViewMixedUpdateRecordsKeepsPerRecordResults(t *testing.T) {
	svc := NewContractService(&PersistenceService{})
	user := &models.UserSession{ID: "005-owner"}

	results, err := svc.UpdateRecords(context.Background(), []contract.Record{
		{
			models.ObjectTypeMetadataKey:       constants.TableListView,
			constants.FieldID:                  "lv-1",
			constants.FieldSysListView_Label:   "Renamed",
			constants.FieldSysListView_Fields:  []string{"Name"},
			constants.FieldSysListView_OwnerID: "005-owner",
		},
		{
			models.ObjectTypeMetadataKey: "Account",
			"Name":                       "Missing Id",
		},
	}, false, user)
	if err != nil {
		t.Fatalf("UpdateRecords mixed batch error = %T %v, want per-record results", err, err)
	}
	if len(results) != 2 {
		t.Fatalf("UpdateRecords mixed results = %#v, want two per-record results", results)
	}
	assertListViewInvalidOperationEntry(t, "UpdateRecords mixed list view row", results[0], "lv-1")
	if results[1].Success {
		t.Fatalf("UpdateRecords mixed Account row success = true, want malformed-id failure")
	}
	if len(results[1].Errors) != 1 || results[1].Errors[0].Kind != contract.RecordMutationErrorMalformedID {
		t.Fatalf("UpdateRecords mixed Account row errors = %#v, want malformed-id failure", results[1].Errors)
	}
}

func TestContractServiceListViewSingleRecordUpsertKeepsLegacyGenericPath(t *testing.T) {
	svc := NewContractService(&PersistenceService{metadata: metadata.NewMetadataService(&stubUpsertMetaRepo{}, nil, nil)})
	user := &models.UserSession{ID: "005-owner"}
	listViewRecord := contract.Record{
		models.ObjectTypeMetadataKey:             constants.TableListView,
		constants.FieldSysListView_ObjectAPIName: "Account",
		constants.FieldSysListView_Label:         "My Accounts",
	}

	_, _, err := svc.UpsertByExternalID(context.Background(), constants.TableListView, constants.FieldSysListView_DeveloperName, "My_Accounts", listViewRecord, user)
	if isListViewInvalidOperation(err) {
		t.Fatalf("UpsertByExternalID(_System_ListView) error = %T %v, want legacy generic path to reach persistence", err, err)
	}
}

func isListViewInvalidOperation(err error) bool {
	var domainErr *apperrors.DomainWireError
	return stderrors.As(err, &domainErr) &&
		domainErr.Kind == apperrors.WireInvalidOperation &&
		strings.Contains(domainErr.Message, constants.TableListView)
}
