package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"context"
	"encoding/json"
	"net/http"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"
)

func TestPruneDeletedTenantReferences_PreservesOtherAllowedTenants(t *testing.T) {
	metadata := map[string]any{
		clientTenantAccessRestrictedKey: true,
		clientAllowedTenantsKey:         []string{"keep-tenant", "deleted-tenant"},
		"tenant_id":                     "deleted-tenant",
	}

	updated, changed, removedOwnerTenantID := pruneDeletedTenantReferences(metadata, map[string]struct{}{
		"deleted-tenant": {},
	})

	require.True(t, changed)
	assert.Equal(t, "deleted-tenant", removedOwnerTenantID)
	assert.Equal(t, true, updated[clientTenantAccessRestrictedKey])
	assert.Equal(t, []string{"keep-tenant"}, updated[clientAllowedTenantsKey])
	_, exists := updated["tenant_id"]
	assert.False(t, exists)
}

func TestPruneDeletedTenantReferences_DisablesRestrictionWhenLastTenantRemoved(t *testing.T) {
	metadata := map[string]any{
		clientTenantAccessRestrictedKey: true,
		clientAllowedTenantsKey:         []string{"deleted-tenant"},
		"tenant_id":                     "deleted-tenant",
	}

	updated, changed, removedOwnerTenantID := pruneDeletedTenantReferences(metadata, map[string]struct{}{
		"deleted-tenant": {},
	})

	require.True(t, changed)
	assert.Equal(t, "deleted-tenant", removedOwnerTenantID)
	assert.Equal(t, false, updated[clientTenantAccessRestrictedKey])
	_, exists := updated[clientAllowedTenantsKey]
	assert.False(t, exists)
	_, exists = updated["tenant_id"]
	assert.False(t, exists)
}

func TestCleanupDeletedTenantReferences_PrunesClientsAndRevokesConsents(t *testing.T) {
	var (
		mu          sync.Mutex
		page0Called bool
		updated     = map[string]map[string]any{}
		revokes     []string
	)

	transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
		mu.Lock()
		defer mu.Unlock()

		switch {
		case req.Method == http.MethodGet && req.URL.Path == "/clients":
			switch req.URL.Query().Get("offset") {
			case "":
				page0Called = true
				return httpJSONAny(req, http.StatusOK, []domain.HydraClient{
					{
						ClientID: "client-keep",
						Metadata: map[string]any{
							clientTenantAccessRestrictedKey: true,
							clientAllowedTenantsKey:         []string{"keep-tenant", "deleted-tenant"},
							"tenant_id":                     "deleted-tenant",
						},
					},
					{
						ClientID: "client-drop",
						Metadata: map[string]any{
							clientTenantAccessRestrictedKey: true,
							clientAllowedTenantsKey:         []string{"deleted-tenant"},
							"tenant_id":                     "deleted-tenant",
						},
					},
				}), nil
			default:
				return httpResponse(req, http.StatusBadRequest, "unexpected offset"), nil
			}

		case req.Method == http.MethodPut && strings.HasPrefix(req.URL.Path, "/clients/"):
			var client domain.HydraClient
			require.NoError(t, json.NewDecoder(req.Body).Decode(&client))
			updated[client.ClientID] = client.Metadata
			return httpJSONAny(req, http.StatusOK, client), nil

		case req.Method == http.MethodDelete && req.URL.Path == "/oauth2/auth/sessions/consent":
			revokes = append(revokes, req.URL.Query().Get("subject")+"|"+req.URL.Query().Get("client"))
			return httpResponse(req, http.StatusNoContent, ""), nil

		default:
			return httpResponse(req, http.StatusNotFound, "unexpected request"), nil
		}
	})

	hydra := &service.HydraAdminService{
		AdminURL:   "http://hydra.test",
		HTTPClient: &http.Client{Transport: transport},
	}
	consentRepo := &mockConsentRepo{
		consents: []domain.ClientConsent{
			{ClientID: "client-keep", Subject: "user-a"},
			{ClientID: "client-drop", Subject: "user-b"},
		},
	}
	outbox := &tenantCleanupMockKetoOutboxRepository{}

	err := cleanupDeletedTenantReferences(context.Background(), hydra, consentRepo, outbox, []string{"deleted-tenant"})
	require.NoError(t, err)
	assert.True(t, page0Called)
	assert.Equal(t, map[string]any{
		clientTenantAccessRestrictedKey: true,
		clientAllowedTenantsKey:         []any{"keep-tenant"},
	}, updated["client-keep"])
	assert.Equal(t, map[string]any{
		clientTenantAccessRestrictedKey: false,
	}, updated["client-drop"])
	assert.ElementsMatch(t, []string{"user-a|client-keep", "user-b|client-drop"}, revokes)
	assert.Empty(t, consentRepo.consents)
	require.Len(t, outbox.entries, 2)
	assert.ElementsMatch(t, []string{"client-keep", "client-drop"}, []string{outbox.entries[0].Object, outbox.entries[1].Object})
	for _, entry := range outbox.entries {
		assert.Equal(t, "RelyingParty", entry.Namespace)
		assert.Equal(t, "parents", entry.Relation)
		assert.Equal(t, "Tenant:deleted-tenant", entry.Subject)
		assert.Equal(t, domain.KetoOutboxActionDelete, entry.Action)
	}
}

type tenantCleanupMockKetoOutboxRepository struct {
	entries []domain.KetoOutbox
}

var _ repository.KetoOutboxRepository = (*tenantCleanupMockKetoOutboxRepository)(nil)

func (m *tenantCleanupMockKetoOutboxRepository) Create(ctx context.Context, entry *domain.KetoOutbox) error {
	if entry == nil {
		return nil
	}
	m.entries = append(m.entries, *entry)
	return nil
}

func (m *tenantCleanupMockKetoOutboxRepository) CreateWithTx(tx *gorm.DB, entry *domain.KetoOutbox) error {
	return m.Create(context.Background(), entry)
}

func (m *tenantCleanupMockKetoOutboxRepository) FindPending(ctx context.Context, limit int) ([]domain.KetoOutbox, error) {
	return nil, nil
}

func (m *tenantCleanupMockKetoOutboxRepository) ListCurrentBySubject(ctx context.Context, namespace, subject string) ([]domain.KetoOutbox, error) {
	return nil, nil
}

func (m *tenantCleanupMockKetoOutboxRepository) UpdateStatus(ctx context.Context, id string, status string, retryCount int, lastError string) error {
	return nil
}

func (m *tenantCleanupMockKetoOutboxRepository) MarkProcessed(ctx context.Context, id string) error {
	return nil
}
