package analytics

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func TestQueryWithSpecPlansCorrelatedMembershipForRepository(t *testing.T) {
	task := &models.ObjectMetadata{
		APIName:      "Task",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "WhatId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}},
			{APIName: "Subject", Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	opportunity := &models.ObjectMetadata{
		APIName:      "Opportunity",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "StageName", Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	repo := &correlatedMembershipCaptureRepo{}
	metadataSvc := metadata.NewMetadataService(&clauseFLSMetadataRepository{schemas: []*models.ObjectMetadata{task, opportunity}}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"task.id":               true,
			"task.whatid":           true,
			"task.subject":          true,
			"task.isdeleted":        true,
			"opportunity.id":        true,
			"opportunity.stagename": true,
			"opportunity.isdeleted": true,
		},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	_, err := qs.QueryWithSpec(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: "Task",
		SelectFields:  []string{constants.FieldID},
		Filter: queryspec.And(
			queryspec.FieldPredicate("Subject", "=", "Call"),
			queryspec.CorrelatedMembershipPredicate(queryspec.CorrelatedMembership{
				ID:                 "semi_0",
				OuterField:         "WhatId",
				InnerObjectAPIName: "Opportunity",
				InnerSelectField:   constants.FieldID,
				InnerFilter:        queryspec.FieldPredicate("StageName", "=", "Prospecting"),
				InnerLimit:         25,
			}),
		),
	}, &models.UserSession{ID: "005-reader", ProfileID: "00e-standard"})
	if err != nil {
		t.Fatalf("QueryWithSpec error = %v", err)
	}
	if repo.findCalled {
		t.Fatal("plain Find was called; want correlated-membership repository path")
	}
	if !repo.membershipCalled {
		t.Fatal("FindWithCorrelatedMemberships was not called")
	}
	if len(repo.plans) != 1 {
		t.Fatalf("plans len = %d, want 1: %#v", len(repo.plans), repo.plans)
	}
	plan := repo.plans[0]
	if plan.ID != "semi_0" || plan.OuterField != "WhatId" || plan.Negate {
		t.Fatalf("outer plan = %#v, want non-negated semi_0 on WhatId", plan)
	}
	if plan.InnerSchema == nil || plan.InnerSchema.APIName != "Opportunity" {
		t.Fatalf("inner schema = %#v, want Opportunity", plan.InnerSchema)
	}
	if plan.InnerSelectField != constants.FieldID {
		t.Fatalf("inner select = %q, want Id", plan.InnerSelectField)
	}
	if plan.InnerRequest.Limit != 25 {
		t.Fatalf("inner limit = %d, want 25", plan.InnerRequest.Limit)
	}
	if len(plan.InnerRequest.Criteria) != 1 || plan.InnerRequest.Criteria[0].Field != "StageName" || plan.InnerRequest.Criteria[0].Val != "Prospecting" {
		t.Fatalf("inner criteria = %#v, want StageName criterion", plan.InnerRequest.Criteria)
	}
	if len(repo.req.Criteria) != 1 || repo.req.Criteria[0].Field != "Subject" {
		t.Fatalf("outer criteria = %#v, want remaining Subject criterion", repo.req.Criteria)
	}
}

func TestQueryWithSpecPlansCorrelatedMembershipForAggregateRepository(t *testing.T) {
	task := &models.ObjectMetadata{
		APIName:      "Task",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "WhatId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	opportunity := &models.ObjectMetadata{
		APIName:      "Opportunity",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "StageName", Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	repo := &correlatedMembershipCaptureRepo{}
	metadataSvc := metadata.NewMetadataService(&clauseFLSMetadataRepository{schemas: []*models.ObjectMetadata{task, opportunity}}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"task.id":               true,
			"task.whatid":           true,
			"task.isdeleted":        true,
			"opportunity.id":        true,
			"opportunity.stagename": true,
			"opportunity.isdeleted": true,
		},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	result, err := qs.QueryWithSpec(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: "Task",
		SelectFields:  []string{"COUNT(Id)"},
		Filter: queryspec.CorrelatedMembershipPredicate(queryspec.CorrelatedMembership{
			ID:                 "semi_0",
			OuterField:         "WhatId",
			InnerObjectAPIName: "Opportunity",
			InnerSelectField:   constants.FieldID,
			InnerFilter:        queryspec.FieldPredicate("StageName", "=", "Prospecting"),
		}),
	}, &models.UserSession{ID: "005-reader", ProfileID: "00e-standard"})
	if err != nil {
		t.Fatalf("QueryWithSpec error = %v", err)
	}
	if repo.runAnalyticsCalled {
		t.Fatal("plain RunAnalytics was called; want correlated-membership aggregate repository path")
	}
	if !repo.analyticsMembershipCalled {
		t.Fatal("RunAnalyticsWithCorrelatedMemberships was not called")
	}
	if len(repo.analyticsPlans) != 1 {
		t.Fatalf("analytics plans len = %d, want 1: %#v", len(repo.analyticsPlans), repo.analyticsPlans)
	}
	if repo.analyticsPlans[0].OuterField != "WhatId" || repo.analyticsPlans[0].InnerSchema.APIName != "Opportunity" {
		t.Fatalf("analytics plan = %#v, want Task.WhatId -> Opportunity", repo.analyticsPlans[0])
	}
	if !result.IsAggregate || len(result.Records) != 1 || result.Records[0]["expr0"] != int64(7) {
		t.Fatalf("result = %#v, want aggregate expr0=7", result)
	}
}

func TestQueryWithSpecPlansCorrelatedMembershipForGroupedAggregateRepository(t *testing.T) {
	task := &models.ObjectMetadata{
		APIName:      "Task",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "WhatId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}},
			{APIName: "Status", Type: constants.FieldTypePicklist},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	opportunity := &models.ObjectMetadata{
		APIName:      "Opportunity",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "StageName", Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	repo := &correlatedMembershipCaptureRepo{}
	metadataSvc := metadata.NewMetadataService(&clauseFLSMetadataRepository{schemas: []*models.ObjectMetadata{task, opportunity}}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"task.id":               true,
			"task.whatid":           true,
			"task.status":           true,
			"task.isdeleted":        true,
			"opportunity.id":        true,
			"opportunity.stagename": true,
			"opportunity.isdeleted": true,
		},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	result, err := qs.QueryWithSpec(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: "Task",
		SelectFields:  []string{"Status", "COUNT(Id)"},
		GroupBy:       "Status",
		Filter: queryspec.CorrelatedMembershipPredicate(queryspec.CorrelatedMembership{
			ID:                 "semi_0",
			OuterField:         "WhatId",
			InnerObjectAPIName: "Opportunity",
			InnerSelectField:   constants.FieldID,
			InnerFilter:        queryspec.FieldPredicate("StageName", "=", "Prospecting"),
		}),
	}, &models.UserSession{ID: "005-reader", ProfileID: "00e-standard"})
	if err != nil {
		t.Fatalf("QueryWithSpec error = %v", err)
	}
	if !repo.analyticsMembershipCalled {
		t.Fatal("RunAnalyticsWithCorrelatedMemberships was not called")
	}
	if repo.analyticsQuery.Operation != constants.AnalyticsOperationGroupBy || repo.analyticsQuery.GroupBy == nil || *repo.analyticsQuery.GroupBy != "Status" {
		t.Fatalf("analytics query = %#v, want group_by Status", repo.analyticsQuery)
	}
	if !result.IsAggregate || len(result.Records) != 1 || result.Records[0]["Status"] != "Open" || result.Records[0]["expr0"] != int64(2) {
		t.Fatalf("result = %#v, want grouped Status=Open expr0=2", result)
	}
}

func TestQueryWithSpecRejectsNestedCorrelatedMembershipForRows(t *testing.T) {
	task := parentFilterObjectSchema("Task",
		models.FieldMetadata{APIName: "WhatId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}},
	)
	opportunity := parentFilterObjectSchema("Opportunity")
	repo := &correlatedMembershipCaptureRepo{}
	metadataSvc := metadata.NewMetadataService(&clauseFLSMetadataRepository{schemas: []*models.ObjectMetadata{task, opportunity}}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	_, err := qs.QueryWithSpec(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: "Task",
		Filter: queryspec.Or(
			queryspec.FieldPredicate("WhatId", "=", "006-direct"),
			queryspec.CorrelatedMembershipPredicate(queryspec.CorrelatedMembership{
				OuterField:         "WhatId",
				InnerObjectAPIName: "Opportunity",
				InnerSelectField:   constants.FieldID,
			}),
		),
	}, &models.UserSession{ID: "005-reader", ProfileID: "00e-standard"})
	if err == nil || !strings.Contains(err.Error(), "correlated membership") || !strings.Contains(err.Error(), "field 'Filter'") {
		t.Fatalf("QueryWithSpec error = %v, want Filter correlated membership shape rejection", err)
	}
}

type correlatedMembershipCaptureRepo struct {
	ports.QueryRepository

	findCalled                bool
	membershipCalled          bool
	runAnalyticsCalled        bool
	analyticsMembershipCalled bool
	req                       models.QueryRequest
	plans                     []ports.QueryCorrelatedMembershipPlan
	analyticsQuery            models.AnalyticsQuery
	analyticsPlans            []ports.QueryCorrelatedMembershipPlan
}

func (r *correlatedMembershipCaptureRepo) Find(ctx context.Context, schema *models.ObjectMetadata, req models.QueryRequest, rlsResult *domainrls.Result, visibleFields []string) (*models.QueryResult, error) {
	r.findCalled = true
	r.req = req
	return &models.QueryResult{Done: true}, nil
}

func (r *correlatedMembershipCaptureRepo) FindWithCorrelatedMemberships(ctx context.Context, schema *models.ObjectMetadata, req models.QueryRequest, rlsResult *domainrls.Result, visibleFields []string, plans []ports.QueryCorrelatedMembershipPlan) (*models.QueryResult, error) {
	r.membershipCalled = true
	r.req = req
	r.plans = append([]ports.QueryCorrelatedMembershipPlan(nil), plans...)
	return &models.QueryResult{Done: true}, nil
}

func (r *correlatedMembershipCaptureRepo) RunAnalytics(ctx context.Context, schema *models.ObjectMetadata, q models.AnalyticsQuery, rlsResult *domainrls.Result) (interface{}, error) {
	r.runAnalyticsCalled = true
	r.analyticsQuery = q
	if q.Operation == constants.AnalyticsOperationGroupBy {
		return []models.SObject{{"name": "Open", "val": int64(2)}}, nil
	}
	return int64(7), nil
}

func (r *correlatedMembershipCaptureRepo) RunAnalyticsWithCorrelatedMemberships(ctx context.Context, schema *models.ObjectMetadata, q models.AnalyticsQuery, rlsResult *domainrls.Result, plans []ports.QueryCorrelatedMembershipPlan) (interface{}, error) {
	r.analyticsMembershipCalled = true
	r.analyticsQuery = q
	r.analyticsPlans = append([]ports.QueryCorrelatedMembershipPlan(nil), plans...)
	if q.Operation == constants.AnalyticsOperationGroupBy {
		return []models.SObject{{"name": "Open", "val": int64(2)}}, nil
	}
	return int64(7), nil
}
