package automation

import (
	"context"
	"fmt"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// reconcileQueryStub serves the four reads reconcileApprovalShares makes:
// ProcessInstance, ProcessDefinition, ProcessInstanceWorkitem, _System_RecordShare.
// The embedded interface satisfies the rest of ports.QueryService; those methods
// panic if called, which the reconciler never does.
type reconcileQueryStub struct {
	ports.QueryService
	targetObjectID string
	processDefID   string
	objectAPIName  string
	workitemActors []string
	existingShares []models.SObject
}

func (q *reconcileQueryStub) QueryWithFilter(ctx context.Context, objectName, filter string, user *models.UserSession, sortField, sortDir string, limit int) (*models.QueryResult, error) {
	switch objectName {
	case constants.TableProcessInstance:
		return reconcileResult(models.SObject{
			constants.FieldID:     "pi-1",
			"TargetObjectId":      q.targetObjectID,
			"ProcessDefinitionId": q.processDefID,
		}), nil
	case constants.TableProcessDefinition:
		return reconcileResult(models.SObject{
			constants.FieldID: q.processDefID,
			"TableEnumOrId":   q.objectAPIName,
		}), nil
	case constants.TableProcessInstanceWorkitem:
		rows := make([]models.SObject, 0, len(q.workitemActors))
		for i, actor := range q.workitemActors {
			rows = append(rows, models.SObject{
				constants.FieldID:   fmt.Sprintf("wi-%d", i+1),
				"ProcessInstanceId": "pi-1",
				"ActorId":           actor,
			})
		}
		return pageReconcileResult(rows, limit), nil
	case constants.TableRecordShare:
		return pageReconcileResult(q.existingShares, limit), nil
	}
	return reconcileResult(), nil
}

// reconcilePersistenceStub records share inserts and deletes. The embedded
// interface satisfies the rest of PersistenceProvider; those methods panic if
// called, which the reconciler never does.
type reconcilePersistenceStub struct {
	PersistenceProvider
	inserts []models.SObject
	deletes []string
}

func (p *reconcilePersistenceStub) Insert(ctx context.Context, tableName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	if tableName == constants.TableRecordShare {
		p.inserts = append(p.inserts, record)
	}
	return record, nil
}
func (p *reconcilePersistenceStub) Delete(ctx context.Context, tableName string, id string, user *models.UserSession) error {
	if tableName == constants.TableRecordShare {
		p.deletes = append(p.deletes, id)
	}
	return nil
}

// pageReconcileResult mimics the query layer's single-read row cap: it returns
// at most `limit` rows and reports Done=false when more rows exist, so a
// reconciler that fails to page would observe a truncated set.
func pageReconcileResult(rows []models.SObject, limit int) *models.QueryResult {
	if limit > 0 && len(rows) > limit {
		return &models.QueryResult{TotalSize: len(rows), Records: rows[:limit], Done: false}
	}
	return &models.QueryResult{TotalSize: len(rows), Records: rows, Done: true}
}

func reconcileResult(records ...models.SObject) *models.QueryResult {
	return &models.QueryResult{TotalSize: len(records), Records: records}
}

func newReconcileTestService(p *reconcilePersistenceStub, q *reconcileQueryStub) *ApprovalService {
	return NewApprovalService(p, q, nil, nil, nil, nil, nil, logging.Default)
}

func approvalShareRow(id, actorID string) models.SObject {
	return models.SObject{
		constants.FieldID:                             id,
		constants.FieldSysRecordShare_ShareWithUserID: actorID,
		constants.FieldSysRecordShare_Reason:          constants.RecordShareReasonApproval,
	}
}

func TestReconcileApprovalSharesGrantsForPendingApprovers(t *testing.T) {
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: []string{"005-a", "005-b"},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.deletes) != 0 {
		t.Fatalf("deletes = %v, want none", p.deletes)
	}
	if len(p.inserts) != 2 {
		t.Fatalf("inserts = %d, want 2", len(p.inserts))
	}
	for _, share := range p.inserts {
		if got := share[constants.FieldSysRecordShare_ObjectAPIName]; got != "GenericRecord" {
			t.Fatalf("share ObjectApiName = %v, want GenericRecord", got)
		}
		if got := share[constants.FieldSysRecordShare_RecordID]; got != "record-1" {
			t.Fatalf("share RecordId = %v, want record-1", got)
		}
		if got := share[constants.FieldSysRecordShare_AccessLevel]; got != constants.PermRead {
			t.Fatalf("share AccessLevel = %v, want %s", got, constants.PermRead)
		}
		if got := share[constants.FieldSysRecordShare_Reason]; got != constants.RecordShareReasonApproval {
			t.Fatalf("share Reason = %v, want %s", got, constants.RecordShareReasonApproval)
		}
	}
}

func TestReconcileApprovalSharesRevokesWhenInstanceTerminal(t *testing.T) {
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: nil, // terminal: workitems already deleted
		existingShares: []models.SObject{approvalShareRow("share-1", "005-a")},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.inserts) != 0 {
		t.Fatalf("inserts = %d, want 0", len(p.inserts))
	}
	if len(p.deletes) != 1 || p.deletes[0] != "share-1" {
		t.Fatalf("deletes = %v, want [share-1]", p.deletes)
	}
}

func TestReconcileApprovalSharesNoOpWhenAligned(t *testing.T) {
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: []string{"005-a"},
		existingShares: []models.SObject{approvalShareRow("share-1", "005-a")},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.inserts) != 0 || len(p.deletes) != 0 {
		t.Fatalf("inserts = %v, deletes = %v, want both empty", p.inserts, p.deletes)
	}
}

func TestReconcileApprovalSharesSwapsOnReassign(t *testing.T) {
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: []string{"005-b"}, // reassigned from a to b
		existingShares: []models.SObject{approvalShareRow("share-1", "005-a")},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.deletes) != 1 || p.deletes[0] != "share-1" {
		t.Fatalf("deletes = %v, want [share-1]", p.deletes)
	}
	if len(p.inserts) != 1 {
		t.Fatalf("inserts = %d, want 1 (for 005-b)", len(p.inserts))
	}
	if got := p.inserts[0][constants.FieldSysRecordShare_ShareWithUserID]; got != "005-b" {
		t.Fatalf("granted share user = %v, want 005-b", got)
	}
}

func TestReconcileApprovalSharesTreatsSoftDeletedAsAbsent(t *testing.T) {
	softDeleted := approvalShareRow("share-1", "005-a")
	softDeleted[constants.FieldSysRecordShare_IsDeleted] = true
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: []string{"005-a"},
		existingShares: []models.SObject{softDeleted},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	// Soft-deleted row is not "current", so a fresh share is granted and nothing is revoked.
	if len(p.inserts) != 1 {
		t.Fatalf("inserts = %d, want 1 (fresh share for 005-a)", len(p.inserts))
	}
	if len(p.deletes) != 0 {
		t.Fatalf("deletes = %v, want none", p.deletes)
	}
}

// TestReconcileApprovalSharesGrantsBeyondSinglePage proves the desired-approver
// set is not truncated at the single-read row cap: an instance with more open
// workitems than one page must still share the record with every approver.
func TestReconcileApprovalSharesGrantsBeyondSinglePage(t *testing.T) {
	const n = 250 // > the 200-row page size
	actors := make([]string, n)
	for i := range actors {
		actors[i] = fmt.Sprintf("005-a-%03d", i)
	}
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: actors,
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.inserts) != n {
		t.Fatalf("inserts = %d, want %d (one per approver, no page truncation)", len(p.inserts), n)
	}
}

// TestReconcileApprovalSharesRevokesBeyondSinglePage proves the current-share
// set is not truncated at the page cap: a terminal instance must revoke every
// active approval share, even past one page.
func TestReconcileApprovalSharesRevokesBeyondSinglePage(t *testing.T) {
	const n = 250
	shares := make([]models.SObject, n)
	for i := range shares {
		shares[i] = approvalShareRow(fmt.Sprintf("share-%03d", i), fmt.Sprintf("005-a-%03d", i))
	}
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: nil, // terminal: every approval share should be revoked
		existingShares: shares,
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.deletes) != n {
		t.Fatalf("deletes = %d, want %d (every active share revoked, no page truncation)", len(p.deletes), n)
	}
}

// TestReconcileApprovalSharesRevokesDuplicateSharesForStaleActor proves a stale
// actor with more than one active share row (e.g. a duplicate left by an earlier
// reconcile) has all of them revoked, not just the first.
func TestReconcileApprovalSharesRevokesDuplicateSharesForStaleActor(t *testing.T) {
	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: nil, // 005-a no longer pending
		existingShares: []models.SObject{
			approvalShareRow("share-1", "005-a"),
			approvalShareRow("share-2", "005-a"),
		},
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.deletes) != 2 {
		t.Fatalf("deletes = %v, want both duplicate rows revoked", p.deletes)
	}
}

// TestReconcileApprovalSharesActiveShareNotCrowdedBySoftDeleted proves a page
// full of soft-deleted history does not hide an active share that sits past the
// first page: the active approver is recognized, so no duplicate is granted.
func TestReconcileApprovalSharesActiveShareNotCrowdedBySoftDeleted(t *testing.T) {
	shares := make([]models.SObject, 0, 201)
	for i := 0; i < 200; i++ { // a full page of soft-deleted history first
		row := approvalShareRow(fmt.Sprintf("dead-%03d", i), fmt.Sprintf("005-old-%03d", i))
		row[constants.FieldSysRecordShare_IsDeleted] = true
		shares = append(shares, row)
	}
	shares = append(shares, approvalShareRow("active-1", "005-a")) // active row past page 1

	p := &reconcilePersistenceStub{}
	q := &reconcileQueryStub{
		targetObjectID: "record-1",
		processDefID:   "pd-1",
		objectAPIName:  "GenericRecord",
		workitemActors: []string{"005-a"}, // still pending; share already exists
		existingShares: shares,
	}
	svc := newReconcileTestService(p, q)

	if err := svc.reconcileApprovalShares(context.Background(), "pi-1", actionUserSession("005-submitter")); err != nil {
		t.Fatalf("reconcileApprovalShares() error = %v", err)
	}
	if len(p.inserts) != 0 {
		t.Fatalf("inserts = %d, want 0 (existing active share must be seen, not crowded out)", len(p.inserts))
	}
	if len(p.deletes) != 0 {
		t.Fatalf("deletes = %v, want none", p.deletes)
	}
}

func actionUserSession(id string) *models.UserSession {
	return &models.UserSession{ID: id, Name: id, ProfileID: constants.ProfileStandardUser}
}
