package analytics

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

// stubQueryService captures QueryWithSpec args and returns a configurable row.
// Implements the subset of ports.QueryService the adapter uses.
type stubQueryService struct {
	gotSpec            queryspec.QuerySpec
	gotSpecUser        *models.UserSession
	gotAnalytics       models.AnalyticsQuery
	returnRows         []models.SObject
	returnRawRows      []models.SObject
	returnAnalytics    interface{}
	returnErr          error
	runAnalyticsErr    error
	rawErr             error
	callCount          int
	specCallCount      int
	analyticsCallCount int
	rawCallCount       int
}

func (s *stubQueryService) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	return nil, errors.New("not used by adapter")
}

func (s *stubQueryService) QueryWithSpec(_ context.Context, spec queryspec.QuerySpec, user *models.UserSession) (*models.QueryResult, error) {
	s.specCallCount++
	s.gotSpec = spec
	s.gotSpecUser = user
	if s.returnErr != nil {
		return nil, s.returnErr
	}
	return &models.QueryResult{Records: s.returnRows}, nil
}

func (s *stubQueryService) QueryWithFilter(ctx context.Context, objectName string, filterExpr string, user *models.UserSession, sortField string, sortDir string, limit int) (*models.QueryResult, error) {
	s.callCount++
	if s.returnErr != nil {
		return nil, s.returnErr
	}
	return &models.QueryResult{Records: s.returnRows}, nil
}

func (s *stubQueryService) RunAnalytics(ctx context.Context, query models.AnalyticsQuery, _ *models.UserSession) (interface{}, error) {
	s.analyticsCallCount++
	s.gotAnalytics = query
	if s.runAnalyticsErr != nil {
		return nil, s.runAnalyticsErr
	}
	if s.returnAnalytics != nil {
		return s.returnAnalytics, nil
	}
	return nil, errors.New("not used")
}

func (s *stubQueryService) SearchSingleObject(ctx context.Context, _ string, _ string, _, _ []string, _, _ int, _, _, _ string, _ *models.UserSession) ([]models.SObject, error) {
	return nil, errors.New("not used")
}

func (s *stubQueryService) SearchSuggestions(context.Context, string, string, []string, []string, int, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (s *stubQueryService) GlobalSearch(ctx context.Context, _ string, _ *models.UserSession) ([]models.SearchResult, error) {
	return nil, errors.New("not used")
}

func (s *stubQueryService) Calculate(ctx context.Context, _ string, _ models.SObject, _ *models.UserSession) (models.SObject, error) {
	return nil, errors.New("not used")
}

func (s *stubQueryService) ExecuteRawSQL(ctx context.Context, _ string, _ []interface{}, _ *models.UserSession) ([]models.SObject, error) {
	s.rawCallCount++
	if s.rawErr != nil {
		return nil, s.rawErr
	}
	if s.returnRawRows != nil {
		return s.returnRawRows, nil
	}
	return nil, errors.New("not used")
}

func stringSlicesEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// TestAdapter_QueryActiveProjectionRow_RoutesThroughQueryWithSpec pins the
// design contract: the adapter MUST go through QueryService.QueryWithSpec
// (which routes through the formula view via QueryRepository.resolveReadTable)
// — NOT through raw QueryRepository.GetRecordsByIDs (which would only select Id
// when fields=nil and would not route through the view, the Codex Pass A R1
// HIGH 1 bug class). The adapter must also carry the caller's explicit active
// formula field list so write-side projection does not read unrelated columns.
func TestAdapter_QueryActiveProjectionRow_RoutesThroughQueryWithSpec(t *testing.T) {
	stub := &stubQueryService{
		returnRows: []models.SObject{
			{"Id": "001x", "Score__c": 99, "StatusActive__c": "Hot"},
		},
	}
	adapter := NewActiveFormulaProjectionAdapter(stub)

	// Pass an explicit user so we can assert it round-trips to QueryWithSpec.
	// Write-side callers pass the user but elevate ctx to SystemContext; the
	// adapter itself just forwards whatever it receives.
	testUser := &models.UserSession{ID: "user-001"}
	row, err := adapter.QueryActiveProjectionRow(context.Background(), testUser, "Account", "001x", false, []string{"Score__c", "StatusActive__c"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if stub.specCallCount != 1 {
		t.Fatalf("QueryWithSpec calls = %d, want 1", stub.specCallCount)
	}
	if stub.gotSpec.ObjectAPIName != "Account" {
		t.Errorf("spec object = %q, want Account", stub.gotSpec.ObjectAPIName)
	}
	if stub.gotSpec.ScanAll {
		t.Error("spec ScanAll = true, want false")
	}
	if stub.gotSpec.Limit != 1 {
		t.Errorf("spec limit = %d, want 1", stub.gotSpec.Limit)
	}
	if got, want := stub.gotSpec.SelectFields, []string{"Score__c", "StatusActive__c"}; !stringSlicesEqual(got, want) {
		t.Errorf("spec select fields = %#v, want %#v", got, want)
	}
	criteria := stub.gotSpec.Filter.Criteria()
	if len(criteria) != 1 || criteria[0].Field != "Id" || criteria[0].Op != "=" || criteria[0].Val != "001x" {
		t.Fatalf("spec criteria = %#v, want Id = 001x", criteria)
	}
	// User must be forwarded verbatim to QueryWithSpec.
	if stub.gotSpecUser != testUser {
		t.Errorf("user: got %v, want testUser", stub.gotSpecUser)
	}
	if row == nil {
		t.Fatal("nil row returned despite stub returning a row")
	}
	if row["StatusActive__c"] != "Hot" {
		t.Errorf("active field missing/wrong: got %v", row["StatusActive__c"])
	}
}

func TestAdapter_QueryActiveProjectionRow_RoutesIncludeDeletedThroughScanAllSpec(t *testing.T) {
	stub := &stubQueryService{
		returnRows: []models.SObject{
			{"Id": "001x", "StatusActive__c": "DeletedHot"},
		},
	}
	adapter := NewActiveFormulaProjectionAdapter(stub)
	testUser := &models.UserSession{ID: "user-001"}

	row, err := adapter.QueryActiveProjectionRow(context.Background(), testUser, "Account", "001x", true, []string{"StatusActive__c"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if stub.specCallCount != 1 {
		t.Fatalf("QueryWithSpec calls = %d, want 1", stub.specCallCount)
	}
	if stub.gotSpec.ObjectAPIName != "Account" {
		t.Errorf("spec object = %q, want Account", stub.gotSpec.ObjectAPIName)
	}
	if !stub.gotSpec.ScanAll {
		t.Error("spec ScanAll = false, want true")
	}
	if stub.gotSpec.Limit != 1 {
		t.Errorf("spec limit = %d, want 1", stub.gotSpec.Limit)
	}
	if got, want := stub.gotSpec.SelectFields, []string{"StatusActive__c"}; !stringSlicesEqual(got, want) {
		t.Errorf("spec select fields = %#v, want %#v", got, want)
	}
	criteria := stub.gotSpec.Filter.Criteria()
	if len(criteria) != 1 || criteria[0].Field != "Id" || criteria[0].Op != "=" || criteria[0].Val != "001x" {
		t.Fatalf("spec criteria = %#v, want Id = 001x", criteria)
	}
	if stub.gotSpecUser != testUser {
		t.Errorf("user: got %v, want testUser", stub.gotSpecUser)
	}
	if row == nil || row["StatusActive__c"] != "DeletedHot" {
		t.Fatalf("row = %#v, want StatusActive__c=DeletedHot", row)
	}
}

func TestAdapter_QueryActiveProjectionRow_EmptyArgsReturnsNil(t *testing.T) {
	stub := &stubQueryService{}
	adapter := NewActiveFormulaProjectionAdapter(stub)

	cases := []struct {
		name       string
		objectName string
		recordID   string
	}{
		{"empty object", "", "001x"},
		{"empty record id", "Account", ""},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			row, err := adapter.QueryActiveProjectionRow(context.Background(), nil, c.objectName, c.recordID, false, []string{"StatusActive__c"})
			if err != nil || row != nil {
				t.Errorf("expected (nil,nil) got (%v,%v)", row, err)
			}
			if stub.specCallCount != 0 {
				t.Errorf("QueryWithSpec called despite empty args: %d", stub.specCallCount)
			}
		})
	}
}

func TestAdapter_QueryActiveProjectionRow_QueryErrorPropagates(t *testing.T) {
	stub := &stubQueryService{returnErr: errors.New("view rebuild in progress")}
	adapter := NewActiveFormulaProjectionAdapter(stub)

	row, err := adapter.QueryActiveProjectionRow(context.Background(), nil, "Account", "001x", false, []string{"StatusActive__c"})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if row != nil {
		t.Errorf("expected nil row on error, got %v", row)
	}
}

func TestAdapter_QueryActiveProjectionRow_NoRowsReturnsNil(t *testing.T) {
	stub := &stubQueryService{returnRows: nil}
	adapter := NewActiveFormulaProjectionAdapter(stub)

	row, err := adapter.QueryActiveProjectionRow(context.Background(), nil, "Account", "001x", false, []string{"StatusActive__c"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if row != nil {
		t.Errorf("expected nil row when no rows returned, got %v", row)
	}
}
