package dialectsfcrm

import (
	"context"
	"errors"
	"reflect"
	"testing"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func TestHistoryQueryResolvesCreatedByName(t *testing.T) {
	query := &historyActorQueryStub{
		history: &contract.HistoryQueryResult{
			TotalSize: 4, Done: true,
			Rows: []contract.HistoryRecord{
				{ID: "017a", ParentID: "001p", Field: "Name", CreatedByID: "005alice"},
				{ID: "017b", ParentID: "001p", Field: "Industry", CreatedByID: "005bob"},
				{ID: "017c", ParentID: "001p", Field: "Vertical__c", CreatedByID: "005alice"}, // dedup
				{ID: "017d", ParentID: "001p", Field: "Rating", CreatedByID: "005ghost"},      // unreadable
			},
		},
		// FLS: 005ghost is intentionally absent from the readable User set.
		users: map[string]string{"005alice": "Alice Adams", "005bob": "Bob Baker"},
	}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query: map[string][]string{
			"q": {"SELECT Id, Field, OldValue, NewValue, CreatedDate, CreatedById, CreatedBy.Name FROM AccountHistory WHERE AccountId = '001p' ORDER BY CreatedDate DESC"},
		},
		User: &models.UserSession{ID: "005reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil {
		t.Fatalf("handler error = %v", err)
	}
	if resp.Status != 200 {
		t.Fatalf("status = %d; body=%#v", resp.Status, resp.Body)
	}

	if !query.userQueried {
		t.Fatal("actor resolution never queried the User object")
	}
	if query.userSpec.ObjectAPIName != historyActorObject || !reflect.DeepEqual(query.userSpec.SelectFields, []string{"Id", "Name"}) {
		t.Fatalf("unexpected user lookup spec: %#v", query.userSpec)
	}
	if want := queryspec.FieldPredicate("Id", "IN", []string{"005alice", "005bob", "005ghost"}); !reflect.DeepEqual(query.userSpec.Filter, want) {
		t.Fatalf("user filter = %#v; want IN over distinct actor ids", query.userSpec.Filter)
	}

	records := historyResponseRecords(t, resp.Body)
	if len(records) != 4 {
		t.Fatalf("records = %d; want 4", len(records))
	}
	assertNestedCreatedBy(t, records[0], "005alice", "Alice Adams")
	assertNestedCreatedBy(t, records[1], "005bob", "Bob Baker")
	assertNestedCreatedBy(t, records[2], "005alice", "Alice Adams")

	// FLS fallback: the unreadable actor keeps its raw id and gets no nested object.
	if records[3]["CreatedById"] != "005ghost" {
		t.Fatalf("row3 CreatedById = %v; want raw 005ghost", records[3]["CreatedById"])
	}
	if _, ok := records[3][historyActorRelationshipName]; ok {
		t.Fatalf("row3 nested CreatedBy present for an unreadable actor: %#v", records[3][historyActorRelationshipName])
	}
}

func TestHistoryQueryOmitsActorResolutionWhenNotSelected(t *testing.T) {
	query := &historyActorQueryStub{
		history: &contract.HistoryQueryResult{Done: true, Rows: []contract.HistoryRecord{
			{ID: "017a", ParentID: "001p", Field: "Name", CreatedByID: "005alice"},
		}},
		users: map[string]string{"005alice": "Alice Adams"},
	}

	resp, err := QueryNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Query:   map[string][]string{"q": {"SELECT Id, Field, CreatedById FROM AccountHistory WHERE AccountId = '001p'"}},
		User:    &models.UserSession{ID: "005reader"},
	}, dialect.Deps{Contracts: contract.Deps{Query: query}})
	if err != nil || resp.Status != 200 {
		t.Fatalf("status=%d err=%v", resp.Status, err)
	}
	if query.userQueried {
		t.Fatal("a bare CreatedById projection must not trigger a User lookup")
	}
	records := historyResponseRecords(t, resp.Body)
	if _, ok := records[0][historyActorRelationshipName]; ok {
		t.Fatal("nested CreatedBy emitted without a CreatedBy projection")
	}
	if records[0]["CreatedById"] != "005alice" {
		t.Fatalf("CreatedById = %v; want 005alice", records[0]["CreatedById"])
	}
}

func TestHistoryActorRelationshipRequested(t *testing.T) {
	cases := []struct {
		fields []string
		want   bool
	}{
		{[]string{"Id", "CreatedById"}, false},
		{[]string{"Id", "CreatedBy.Name"}, true},
		{[]string{"createdby.name"}, true},
		{[]string{"CreatedBy"}, true},
		{[]string{"CreatedByIdSomething"}, false},
	}
	for _, tc := range cases {
		if got := historyActorRelationshipRequested(tc.fields); got != tc.want {
			t.Fatalf("historyActorRelationshipRequested(%v) = %v; want %v", tc.fields, got, tc.want)
		}
	}
}

func historyResponseRecords(t *testing.T, body any) []map[string]any {
	t.Helper()
	envelope, ok := body.(map[string]any)
	if !ok {
		t.Fatalf("response body is not an envelope: %#v", body)
	}
	raw, ok := envelope["records"].([]any)
	if !ok {
		t.Fatalf("response records not []any: %#v", envelope["records"])
	}
	out := make([]map[string]any, 0, len(raw))
	for _, item := range raw {
		switch record := item.(type) {
		case models.SObject:
			out = append(out, map[string]any(record))
		case map[string]any:
			out = append(out, record)
		default:
			t.Fatalf("record is not a map: %#v", item)
		}
	}
	return out
}

func assertNestedCreatedBy(t *testing.T, record map[string]any, wantID, wantName string) {
	t.Helper()
	if record[historyActorIDField] != wantID {
		t.Fatalf("CreatedById = %v; want %q", record[historyActorIDField], wantID)
	}
	nested, ok := record[historyActorRelationshipName].(map[string]any)
	if !ok {
		t.Fatalf("nested CreatedBy missing/wrong type: %#v", record[historyActorRelationshipName])
	}
	if nested["Name"] != wantName {
		t.Fatalf("CreatedBy.Name = %v; want %q", nested["Name"], wantName)
	}
	if nested["Id"] != wantID {
		t.Fatalf("CreatedBy.Id = %v; want %q", nested["Id"], wantID)
	}
	attrs, ok := nested["attributes"].(map[string]any)
	if !ok || attrs["type"] != historyActorObject {
		t.Fatalf("CreatedBy.attributes.type = %#v; want User", nested["attributes"])
	}
}

type historyActorQueryStub struct {
	history *contract.HistoryQueryResult
	users   map[string]string

	userQueried bool
	userSpec    queryspec.QuerySpec
}

func (s *historyActorQueryStub) Query(_ context.Context, spec queryspec.QuerySpec, _ *models.UserSession) (*contract.QueryResult, error) {
	s.userQueried = true
	s.userSpec = spec
	ids, _ := spec.Filter.Value.([]string)
	records := make([]contract.Record, 0, len(ids))
	for _, id := range ids {
		if name, ok := s.users[id]; ok {
			records = append(records, contract.Record{"Id": id, "Name": name})
		}
	}
	return &contract.QueryResult{TotalSize: len(records), Done: true, Records: records}, nil
}

func (s *historyActorQueryStub) SearchWithSpec(context.Context, queryspec.SearchSpec, *models.UserSession) ([]contract.Record, error) {
	return nil, errors.New("unexpected SearchWithSpec call")
}

func (s *historyActorQueryStub) GlobalSearchWithSpec(context.Context, queryspec.GlobalSearchSpec, *models.UserSession) ([]contract.SearchResult, error) {
	return nil, errors.New("unexpected GlobalSearchWithSpec call")
}

func (s *historyActorQueryStub) QueryHistory(context.Context, contract.HistoryQueryRequest, *models.UserSession) (*contract.HistoryQueryResult, error) {
	return s.history, nil
}
