package manager

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	"github.com/netbirdio/netbird/management/server/store"
)

func TestReapExpiredExposes(t *testing.T) {
	mgr, testStore := setupIntegrationTest(t)
	ctx := context.Background()

	resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 8080,
		Mode: "http",
	})
	require.NoError(t, err)

	// Manually expire the service by backdating meta_last_renewed_at
	expireEphemeralService(t, testStore, testAccountID, resp.Domain)

	// Create a non-expired service
	resp2, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 8081,
		Mode: "http",
	})
	require.NoError(t, err)

	mgr.exposeReaper.reapExpiredExposes(ctx)

	// Expired service should be deleted
	_, err = testStore.GetServiceByDomain(ctx, resp.Domain)
	require.Error(t, err, "expired service should be deleted")

	// Non-expired service should remain
	_, err = testStore.GetServiceByDomain(ctx, resp2.Domain)
	require.NoError(t, err, "active service should remain")
}

func TestReapAlreadyDeletedService(t *testing.T) {
	mgr, testStore := setupIntegrationTest(t)
	ctx := context.Background()

	resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 8080,
		Mode: "http",
	})
	require.NoError(t, err)

	expireEphemeralService(t, testStore, testAccountID, resp.Domain)

	// Delete the service before reaping
	svcID := resolveServiceIDByDomain(t, testStore, resp.Domain)
	err = mgr.StopServiceFromPeer(ctx, testAccountID, testPeerID, svcID)
	require.NoError(t, err)

	// Reaping should handle the already-deleted service gracefully
	mgr.exposeReaper.reapExpiredExposes(ctx)
}

func TestConcurrentReapAndRenew(t *testing.T) {
	mgr, testStore := setupIntegrationTest(t)
	ctx := context.Background()

	for i := range 5 {
		_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
			Port: uint16(8080 + i),
			Mode: "http",
		})
		require.NoError(t, err)
	}

	// Expire all services
	services, err := testStore.GetAccountServices(ctx, store.LockingStrengthNone, testAccountID)
	require.NoError(t, err)
	for _, svc := range services {
		if svc.Source == rpservice.SourceEphemeral {
			expireEphemeralService(t, testStore, testAccountID, svc.Domain)
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		mgr.exposeReaper.reapExpiredExposes(ctx)
	}()
	go func() {
		defer wg.Done()
		_, _ = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
	}()
	wg.Wait()

	count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count, "all expired services should be reaped")
}

func TestRenewEphemeralService(t *testing.T) {
	mgr, _ := setupIntegrationTest(t)
	ctx := context.Background()

	t.Run("renew succeeds for active service", func(t *testing.T) {
		resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
			Port: 8082,
			Mode: "http",
		})
		require.NoError(t, err)

		svc, lookupErr := mgr.store.GetServiceByDomain(ctx, resp.Domain)
		require.NoError(t, lookupErr)
		err = mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, svc.ID)
		require.NoError(t, err)
	})

	t.Run("renew fails for nonexistent domain", func(t *testing.T) {
		err := mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, "nonexistent-service-id")
		require.Error(t, err)
		assert.Contains(t, err.Error(), "no active expose session")
	})
}

func TestCountAndExistsEphemeralServices(t *testing.T) {
	mgr, _ := setupIntegrationTest(t)
	ctx := context.Background()

	count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
	require.NoError(t, err)
	assert.Equal(t, int64(0), count)

	resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 8083,
		Mode: "http",
	})
	require.NoError(t, err)

	count, err = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
	require.NoError(t, err)
	assert.Equal(t, int64(1), count)

	exists, err := mgr.store.EphemeralServiceExists(ctx, store.LockingStrengthNone, testAccountID, testPeerID, resp.Domain)
	require.NoError(t, err)
	assert.True(t, exists, "service should exist")

	exists, err = mgr.store.EphemeralServiceExists(ctx, store.LockingStrengthNone, testAccountID, testPeerID, "no-such.domain")
	require.NoError(t, err)
	assert.False(t, exists, "non-existent service should not exist")
}

func TestMaxExposesPerPeerEnforced(t *testing.T) {
	mgr, _ := setupIntegrationTest(t)
	ctx := context.Background()

	for i := range maxExposesPerPeer {
		_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
			Port: uint16(8090 + i),
			Mode: "http",
		})
		require.NoError(t, err, "expose %d should succeed", i)
	}

	_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 9999,
		Mode: "http",
	})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "maximum number of active expose sessions")
}

func TestReapSkipsRenewedService(t *testing.T) {
	mgr, testStore := setupIntegrationTest(t)
	ctx := context.Background()

	resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
		Port: 8086,
		Mode: "http",
	})
	require.NoError(t, err)

	// Expire the service
	expireEphemeralService(t, testStore, testAccountID, resp.Domain)

	// Renew it before the reaper runs
	svc, err := testStore.GetServiceByDomain(ctx, resp.Domain)
	require.NoError(t, err)
	err = mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, svc.ID)
	require.NoError(t, err)

	// Reaper should skip it because the re-check sees a fresh timestamp
	mgr.exposeReaper.reapExpiredExposes(ctx)

	_, err = testStore.GetServiceByDomain(ctx, resp.Domain)
	require.NoError(t, err, "renewed service should survive reaping")
}

// resolveServiceIDByDomain looks up a service ID by domain in tests.
func resolveServiceIDByDomain(t *testing.T, s store.Store, domain string) string {
	t.Helper()
	svc, err := s.GetServiceByDomain(context.Background(), domain)
	require.NoError(t, err)
	return svc.ID
}

// expireEphemeralService backdates meta_last_renewed_at to force expiration.
func expireEphemeralService(t *testing.T, s store.Store, accountID, domain string) {
	t.Helper()
	svc, err := s.GetServiceByDomain(context.Background(), domain)
	require.NoError(t, err)

	expired := time.Now().Add(-2 * exposeTTL)
	svc.Meta.LastRenewedAt = &expired
	err = s.UpdateService(context.Background(), svc)
	require.NoError(t, err)
}
