package identityruntime

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

type tenantSyncRepository struct {
	targets     []store.OIDCTenantBindingSyncTarget
	synced      []string
	unchanged   []string
	rejected    []string
	failed      []string
	failureNext time.Time
}

func (repository *tenantSyncRepository) DueOIDCTenantBindingSyncs(context.Context, string, string, int) ([]store.OIDCTenantBindingSyncTarget, error) {
	return repository.targets, nil
}
func (repository *tenantSyncRepository) ApplyOIDCTenantBindingSync(_ context.Context, id string, _ identity.TenantContext, unchanged bool, _ time.Time) error {
	if unchanged {
		repository.unchanged = append(repository.unchanged, id)
	} else {
		repository.synced = append(repository.synced, id)
	}
	return nil
}
func (repository *tenantSyncRepository) RejectOIDCTenantBinding(_ context.Context, id, _ string, _ time.Time) error {
	repository.rejected = append(repository.rejected, id)
	return nil
}
func (repository *tenantSyncRepository) FailOIDCTenantBindingSync(_ context.Context, id, _ string, next time.Time) error {
	repository.failed = append(repository.failed, id)
	repository.failureNext = next
	return nil
}

type tenantSyncReader struct {
	tenants   map[string]identity.TenantContext
	unchanged map[string]bool
	errors    map[string]error
}

func (reader tenantSyncReader) Get(_ context.Context, tenantID, _ string) (identity.TenantContext, bool, error) {
	return reader.tenants[tenantID], reader.unchanged[tenantID], reader.errors[tenantID]
}

func TestSynchronizeDueTenantContextsAppliesETagRejectsAndBacksOff(t *testing.T) {
	now := time.Unix(1_780_000_000, 0).UTC()
	repository := &tenantSyncRepository{targets: []store.OIDCTenantBindingSyncTarget{
		{ID: "binding-a", ExternalTenantID: "tenant-a", ETag: `"a"`, FailureCount: 0},
		{ID: "binding-b", ExternalTenantID: "tenant-b", ETag: `"b"`, FailureCount: 0},
		{ID: "binding-c", ExternalTenantID: "tenant-c", FailureCount: 3},
		{ID: "binding-d", ExternalTenantID: "tenant-d", FailureCount: 0},
	}}
	reader := tenantSyncReader{
		tenants: map[string]identity.TenantContext{
			"tenant-a": {TenantStatus: "active", TenantApplicationStatus: "active"},
			"tenant-d": {TenantStatus: "suspended", TenantApplicationStatus: "active"},
		},
		unchanged: map[string]bool{"tenant-b": true},
		errors: map[string]error{
			"tenant-c": identity.ErrTenantContextUnavailable,
		},
	}
	if err := synchronizeDueTenantContexts(context.Background(), repository, reader, "issuer", "application", now); err != nil {
		t.Fatal(err)
	}
	if len(repository.synced) != 1 || repository.synced[0] != "binding-a" ||
		len(repository.unchanged) != 1 || repository.unchanged[0] != "binding-b" ||
		len(repository.rejected) != 1 || repository.rejected[0] != "binding-d" ||
		len(repository.failed) != 1 || repository.failed[0] != "binding-c" {
		t.Fatalf("sync outcomes=%#v", repository)
	}
	if repository.failureNext.Sub(now) != 4*time.Minute {
		t.Fatalf("retry delay=%s", repository.failureNext.Sub(now))
	}
}

func TestSynchronizeDueTenantContextsTreatsNotFoundAsRevocation(t *testing.T) {
	repository := &tenantSyncRepository{targets: []store.OIDCTenantBindingSyncTarget{
		{ID: "binding-a", ExternalTenantID: "tenant-a"},
	}}
	reader := tenantSyncReader{
		tenants: map[string]identity.TenantContext{}, unchanged: map[string]bool{},
		errors: map[string]error{"tenant-a": identity.ErrTenantContextNotFound},
	}
	if err := synchronizeDueTenantContexts(
		context.Background(), repository, reader, "issuer", "application", time.Now(),
	); err != nil {
		t.Fatal(err)
	}
	if len(repository.rejected) != 1 || len(repository.failed) != 0 {
		t.Fatalf("not-found outcome=%#v", repository)
	}
}

func TestTenantContextRetryDelayIsBounded(t *testing.T) {
	if got := tenantContextRetryDelay(0); got != 30*time.Second {
		t.Fatalf("first retry=%s", got)
	}
	if got := tenantContextRetryDelay(100); got != 5*time.Minute {
		t.Fatalf("bounded retry=%s", got)
	}
	if !errors.Is(identity.ErrTenantContextUnavailable, identity.ErrTenantContextUnavailable) {
		t.Fatal("sentinel error changed unexpectedly")
	}
}
