package records

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/apierr"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordviewv1"
)

type runtimeRecordViewEngineStub struct {
	mu                sync.Mutex
	row               models.Record
	readable          map[string]bool
	retrieveCalls     int
	retrieveByCalls   int
	retrieveManyRows  map[string]map[string]models.Record
	retrieveManyErr   map[string]error
	retrieveManyCalls map[string]int
	parallelTarget    int
	parallelActive    int
	parallelMax       int
	allStarted        chan struct{}
	allStartedOnce    sync.Once
	releaseParallel   chan struct{}
}

func (s *runtimeRecordViewEngineStub) RetrieveWithAccess(_ context.Context, _, _ string, _ *models.UserSession) (models.Record, *semantic.RecordAccess, error) {
	s.mu.Lock()
	s.retrieveCalls++
	s.mu.Unlock()
	return s.row, &semantic.RecordAccess{Read: true, Update: true}, nil
}

func (s *runtimeRecordViewEngineStub) RetrieveByField(_ context.Context, _, _, _ string, _ *models.UserSession) (models.Record, error) {
	s.mu.Lock()
	s.retrieveByCalls++
	s.mu.Unlock()
	return s.row, nil
}

func (s *runtimeRecordViewEngineStub) ReadReferenceDisplayProjection(ctx context.Context, schema *models.ObjectMetadata, _ []string, _ *models.UserSession) (map[string]models.Record, error) {
	objectID := schema.APIName
	s.mu.Lock()
	if s.retrieveManyCalls == nil {
		s.retrieveManyCalls = map[string]int{}
	}
	s.retrieveManyCalls[objectID]++
	s.parallelActive++
	if s.parallelActive > s.parallelMax {
		s.parallelMax = s.parallelActive
	}
	if s.parallelTarget > 0 && s.parallelActive >= s.parallelTarget {
		s.allStartedOnce.Do(func() { close(s.allStarted) })
	}
	rows, retrieveErr := s.retrieveManyRows[objectID], s.retrieveManyErr[objectID]
	release := s.releaseParallel
	s.mu.Unlock()
	if release != nil {
		select {
		case <-release:
		case <-ctx.Done():
			retrieveErr = ctx.Err()
		}
	}
	s.mu.Lock()
	s.parallelActive--
	s.mu.Unlock()
	return rows, retrieveErr
}

func (s *runtimeRecordViewEngineStub) FieldReadable(_ context.Context, _, fieldID string, _ *models.UserSession) (bool, error) {
	return s.readable[fieldID], nil
}

type runtimeRecordViewActivityStub struct{ calls int }

func (s *runtimeRecordViewActivityStub) TrackRecordAccess(context.Context, string, string, string) {
	s.calls++
}

func TestRuntimeRecordViewUsesBoundV2FieldsAndPreservesActivity(t *testing.T) {
	view := runtimeAttachmentSourceView(t)
	engine := &runtimeRecordViewEngineStub{
		row:      models.SObject{constants.FieldID: "work-1", "DocumentLink": `<a href="/download/ABCDEF123456">doc</a>`, "DocumentPublicationId": "publication-1", "PrivatePhysical": "never-returned"},
		readable: map[string]bool{"DocumentLink": true},
		retrieveManyRows: map[string]map[string]models.Record{
			"DocumentPublication": {"publication-1": models.SObject{constants.FieldID: "publication-1", "Name": "Published document"}},
		},
	}
	activity := &runtimeRecordViewActivityStub{}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine, Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: activity,
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	result, err := service.ReadRecordView(ctx, semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionEnrichedView,
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatal(err)
	}
	if result.RecordID != "work-1" || result.Root["PrivatePhysical"] != "never-returned" || len(result.AttachmentSourceFields) != 1 ||
		result.AttachmentSourceFields[0].SourceFieldID != "DocumentLink" || result.AttachmentSourceFields[0].ResolverID != "document_publication" ||
		result.Access == nil || !result.Access.Read || !result.Access.Update || engine.retrieveCalls != 1 || activity.calls != 1 {
		t.Fatalf("result=%#v activity=%d", result, activity.calls)
	}
	if len(result.ReferenceDisplays) != 1 || result.ReferenceDisplays[0].SourceFieldID != "DocumentPublicationId" || result.ReferenceDisplays[0].TargetObjectID != "DocumentPublication" || result.ReferenceDisplays[0].TargetRecordID != "publication-1" || result.ReferenceDisplays[0].DisplayName != "Published document" {
		t.Fatalf("reference displays = %#v", result.ReferenceDisplays)
	}
}

func TestRuntimeRecordViewRejectsHiddenFieldSelectorBeforeRepositoryAccess(t *testing.T) {
	view := runtimeAttachmentSourceView(t)
	engine := &runtimeRecordViewEngineStub{readable: map[string]bool{"DocumentLink": false}}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine, Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	_, err = service.ReadRecordView(ctx, semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorFieldEqual, FieldID: "DocumentLink", FieldValue: "guess"}, Projection: semantic.ProjectionRoot,
	}, &models.UserSession{ID: "user-1"})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureForbidden || engine.retrieveByCalls != 0 {
		t.Fatalf("err=%v retrieveBy=%d", err, engine.retrieveByCalls)
	}
}

func TestRuntimeRecordViewMasksOnlyInaccessibleReferenceDisplays(t *testing.T) {
	view := runtimeAttachmentSourceView(t)
	root := models.SObject{constants.FieldID: "work-1", "DocumentPublicationId": "publication-1"}
	for name, retrieveErr := range map[string]error{
		"forbidden related object": apierr.Forbidden("related object is not readable"),
		"missing related record":   apierr.NotFound("related record is missing"),
	} {
		t.Run(name, func(t *testing.T) {
			engine := &runtimeRecordViewEngineStub{
				row: root, retrieveManyErr: map[string]error{"DocumentPublication": retrieveErr},
			}
			service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
				Engine: engine, ReferenceReader: engine,
				Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
			})
			if err != nil {
				t.Fatal(err)
			}
			result, err := service.ReadRecordView(context.Background(), semantic.ReadRequest{
				ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionEnrichedView,
			}, &models.UserSession{ID: "user-1"})
			if err != nil || len(result.ReferenceDisplays) != 0 {
				t.Fatalf("result=%#v err=%v", result, err)
			}
		})
	}
}

func TestRuntimeRecordViewPropagatesRelatedRecordFailure(t *testing.T) {
	view := runtimeAttachmentSourceView(t)
	want := errors.New("repository unavailable")
	engine := &runtimeRecordViewEngineStub{
		row:             models.SObject{constants.FieldID: "work-1", "DocumentPublicationId": "publication-1"},
		retrieveManyErr: map[string]error{"DocumentPublication": want},
	}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine,
		Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
	})
	if err != nil {
		t.Fatal(err)
	}
	_, err = service.ReadRecordView(context.Background(), semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionEnrichedView,
	}, &models.UserSession{ID: "user-1"})
	if !errors.Is(err, want) {
		t.Fatalf("err=%v, want %v", err, want)
	}
}

func TestRuntimeRecordViewRetrievesIndependentReferenceObjectsWithBoundedParallelism(t *testing.T) {
	view := runtimeParallelReferenceView(t)
	release := make(chan struct{})
	allStarted := make(chan struct{})
	engine := &runtimeRecordViewEngineStub{
		row: models.SObject{
			constants.FieldID: "work-1",
			"AccountId":       "account-1",
			"CampaignId":      "campaign-1",
			"OwnerId":         "user-1",
		},
		retrieveManyRows: map[string]map[string]models.Record{
			"Account":  {"account-1": models.SObject{constants.FieldID: "account-1", "Name": "Account One"}},
			"Campaign": {"campaign-1": models.SObject{constants.FieldID: "campaign-1", "Name": "Campaign One"}},
			"User": {"user-1": models.SObject{
				constants.FieldID: "user-1", "Username": "barnett.su@example.test",
				"Email": "contact-address@example.test", "FirstName": "Barnett", "LastName": "Su",
			}},
		},
		parallelTarget:  3,
		allStarted:      allStarted,
		releaseParallel: release,
	}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine, Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
	})
	if err != nil {
		t.Fatal(err)
	}
	type readResult struct {
		result *RuntimeRecordViewResult
		err    error
	}
	completed := make(chan readResult, 1)
	go func() {
		result, readErr := service.ReadRecordView(context.Background(), semantic.ReadRequest{
			ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionEnrichedView,
		}, &models.UserSession{ID: "reader"})
		completed <- readResult{result: result, err: readErr}
	}()

	select {
	case <-allStarted:
		close(release)
	case <-time.After(time.Second):
		close(release)
		<-completed
		t.Fatal("independent reference objects did not overlap")
	}
	got := <-completed
	if got.err != nil {
		t.Fatal(got.err)
	}
	if engine.parallelMax != 3 {
		t.Fatalf("max concurrent reference reads = %d, want 3", engine.parallelMax)
	}
	if len(got.result.ReferenceDisplays) != 3 ||
		got.result.ReferenceDisplays[0].SourceFieldID != "AccountId" ||
		got.result.ReferenceDisplays[1].SourceFieldID != "CampaignId" ||
		got.result.ReferenceDisplays[2].SourceFieldID != "OwnerId" ||
		got.result.ReferenceDisplays[2].DisplayName != "Barnett Su" {
		t.Fatalf("reference displays = %#v, want deterministic source-field order", got.result.ReferenceDisplays)
	}
}

func TestRuntimeRecordViewDoesNotQueryMetadataOwnedRecordTypeAsRecord(t *testing.T) {
	view := runtimeRecordTypeReferenceView(t)
	engine := &runtimeRecordViewEngineStub{
		row: models.SObject{
			constants.FieldID:           "work-1",
			constants.FieldRecordTypeID: "record-type-1",
		},
	}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine, Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
	})
	if err != nil {
		t.Fatal(err)
	}
	result, err := service.ReadRecordView(context.Background(), semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionEnrichedView,
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatal(err)
	}
	if got := result.Root[constants.FieldRecordTypeID]; got != "record-type-1" {
		t.Fatalf("RecordTypeId = %#v, want record-type-1", got)
	}
	if len(result.ReferenceDisplays) != 0 || engine.retrieveManyCalls[constants.ObjectRecordType] != 0 {
		t.Fatalf("reference displays = %#v, RecordType RetrieveMany calls = %d", result.ReferenceDisplays, engine.retrieveManyCalls[constants.ObjectRecordType])
	}
}

func TestRuntimeRecordViewChecksCancellationBeforeAnyEngineAccess(t *testing.T) {
	view := runtimeAttachmentSourceView(t)
	engine := &runtimeRecordViewEngineStub{}
	service, err := NewRuntimeRecordViewService(RuntimeRecordViewConfig{
		Engine: engine, ReferenceReader: engine, Metadata: runtimeAttachmentSourceMetadataStub{view: view}, Activity: &runtimeRecordViewActivityStub{},
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = service.ReadRecordView(ctx, semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "work-1"}, Projection: semantic.ProjectionRoot,
	}, &models.UserSession{ID: "user-1"})
	if !errors.Is(err, context.Canceled) || engine.retrieveCalls != 0 {
		t.Fatalf("err=%v retrieve=%d", err, engine.retrieveCalls)
	}
}

func runtimeParallelReferenceView(t *testing.T) *catalogsnapshot.RuntimeMetadataView {
	t.Helper()
	const revision catalogsnapshot.CatalogRevision = 1
	root := &models.ObjectMetadata{ID: "work-item", APIName: "WorkItem", Label: "Work Item", PluralLabel: "Work Items", SharingModel: constants.SharingModelPublicReadWrite, Fields: []models.FieldMetadata{
		{ID: "wi-id", APIName: constants.FieldID, Label: "ID", Type: constants.FieldTypeString, PhysicalName: "id", IsSystem: true},
		{ID: "wi-account", APIName: "AccountId", Label: "Account", Type: constants.FieldTypeReference, PhysicalName: "account_id", ReferenceTo: []string{"Account"}},
		{ID: "wi-campaign", APIName: "CampaignId", Label: "Campaign", Type: constants.FieldTypeReference, PhysicalName: "campaign_id", ReferenceTo: []string{"Campaign"}},
		{ID: "wi-owner", APIName: "OwnerId", Label: "Owner", Type: constants.FieldTypeReference, PhysicalName: "owner_id", ReferenceTo: []string{"User"}},
	}}
	objects := []catalogsnapshot.ObjectInput{{CatalogRevision: revision, Metadata: root}}
	objectNames := []string{"WorkItem"}
	for _, name := range []string{"Account", "Campaign", "User"} {
		fields := []models.FieldMetadata{
			{ID: strings.ToLower(name) + "-id", APIName: constants.FieldID, Label: "ID", Type: constants.FieldTypeString, PhysicalName: "id", IsSystem: true},
			{ID: strings.ToLower(name) + "-name", APIName: "Name", Label: "Name", Type: constants.FieldTypeString, PhysicalName: "name", IsNameField: true},
		}
		if name == constants.TableUser {
			fields = []models.FieldMetadata{
				{ID: "user-id", APIName: constants.FieldID, Label: "ID", Type: constants.FieldTypeString, PhysicalName: "id", IsSystem: true},
				{ID: "user-username", APIName: constants.FieldUsername, Label: "Username", Type: constants.FieldTypeString, PhysicalName: "username", IsNameField: true},
				{ID: "user-email", APIName: constants.FieldEmail, Label: "Email", Type: constants.FieldTypeEmail, PhysicalName: "email", IsNameField: true},
				{ID: "user-first", APIName: constants.FieldFirstName, Label: "First Name", Type: constants.FieldTypeString, PhysicalName: "first_name"},
				{ID: "user-last", APIName: constants.FieldLastName, Label: "Last Name", Type: constants.FieldTypeString, PhysicalName: "last_name"},
			}
		}
		objects = append(objects, catalogsnapshot.ObjectInput{CatalogRevision: revision, Metadata: &models.ObjectMetadata{
			ID: strings.ToLower(name), APIName: name, Label: name, PluralLabel: name + "s", SharingModel: constants.SharingModelPublicReadWrite,
			Fields: fields,
		}})
		objectNames = append(objectNames, name)
	}
	artifact, err := catalogsnapshot.CompileCatalogSnapshot(catalogsnapshot.CompileInput{
		OrgID: "org-parallel-record-view", Lineage: catalogsnapshot.Lineage{CatalogRevision: revision, SchemaGeneration: 1, EngineGeneration: 1},
		Objects:     objects,
		Projections: []catalogsnapshot.RuntimeProjectionInput{{CatalogRevision: revision, AppInstallationID: "installation-record-view", CapabilityID: semantic.CapabilityName, Profile: catalogsnapshot.ProjectionRecordViewV1, ObjectAPINames: objectNames}},
	})
	if err != nil {
		t.Fatal(err)
	}
	snapshot, err := catalogsnapshot.OpenCatalogSnapshot(artifact)
	if err != nil {
		t.Fatal(err)
	}
	projection, err := catalogsnapshot.ProjectRuntimeMetadataView(snapshot, catalogsnapshot.ProjectionKey{AppInstallationID: "installation-record-view", CapabilityID: semantic.CapabilityName})
	if err != nil {
		t.Fatal(err)
	}
	view, err := catalogsnapshot.OpenRuntimeMetadataView(projection)
	if err != nil {
		t.Fatal(err)
	}
	return view
}
