package management

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"nhooyr.io/websocket"

	"github.com/cloudflare/cloudflared/internal/test"
)

var (
	noopLogger         = zerolog.New(io.Discard)
	managementHostname = "https://management.argotunnel.com"
)

func TestDisableDiagnosticRoutes(t *testing.T) {
	mgmt := New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &noopLogger, nil)
	for _, path := range []string{"/metrics", "/debug/pprof/goroutine", "/debug/pprof/heap"} {
		t.Run(strings.Replace(path, "/", "_", -1), func(t *testing.T) {
			req := httptest.NewRequest("GET", managementHostname+path+"?access_token="+validToken, nil)
			recorder := httptest.NewRecorder()
			mgmt.ServeHTTP(recorder, req)
			resp := recorder.Result()
			require.Equal(t, http.StatusNotFound, resp.StatusCode)
		})
	}
}

func TestReadEventsLoop(t *testing.T) {
	sentEvent := EventStartStreaming{
		ClientEvent: ClientEvent{Type: StartStreaming},
	}
	client, server := test.WSPipe(nil, nil)
	client.CloseRead(context.Background())
	defer func() {
		client.Close(websocket.StatusInternalError, "")
	}()
	go func() {
		err := WriteEvent(client, context.Background(), &sentEvent)
		require.NoError(t, err)
	}()
	m := ManagementService{
		log: &noopLogger,
	}
	events := make(chan *ClientEvent)
	go m.readEvents(server, context.Background(), events)
	event := <-events
	require.Equal(t, sentEvent.Type, event.Type)
	server.Close(websocket.StatusInternalError, "")
}

func TestReadEventsLoop_ContextCancelled(t *testing.T) {
	client, server := test.WSPipe(nil, nil)
	ctx, cancel := context.WithCancel(context.Background())
	client.CloseRead(ctx)
	defer func() {
		client.Close(websocket.StatusInternalError, "")
	}()
	m := ManagementService{
		log: &noopLogger,
	}
	events := make(chan *ClientEvent)
	go func() {
		time.Sleep(time.Second)
		cancel()
	}()
	// Want to make sure this function returns when context is cancelled
	m.readEvents(server, ctx, events)
	server.Close(websocket.StatusInternalError, "")
}

func TestCanStartStream_NoSessions(t *testing.T) {
	m := ManagementService{
		log: &noopLogger,
		logger: &Logger{
			Log: &noopLogger,
		},
	}
	_, cancel := context.WithCancel(context.Background())
	session := newSession(0, actor{}, cancel)
	assert.True(t, m.canStartStream(session))
}

func TestCanStartStream_ExistingSessionDifferentActor(t *testing.T) {
	m := ManagementService{
		log: &noopLogger,
		logger: &Logger{
			Log: &noopLogger,
		},
	}
	_, cancel := context.WithCancel(context.Background())
	session1 := newSession(0, actor{ID: "test"}, cancel)
	assert.True(t, m.canStartStream(session1))
	m.logger.Listen(session1)
	assert.True(t, session1.Active())

	// Try another session
	session2 := newSession(0, actor{ID: "test2"}, cancel)
	assert.Equal(t, 1, m.logger.ActiveSessions())
	assert.False(t, m.canStartStream(session2))

	// Close session1
	m.logger.Remove(session1)
	assert.True(t, session1.Active()) // Remove doesn't stop a session
	session1.Stop()
	assert.False(t, session1.Active())
	assert.Equal(t, 0, m.logger.ActiveSessions())

	// Try session2 again
	assert.True(t, m.canStartStream(session2))
}

func TestCanStartStream_ExistingSessionSameActor(t *testing.T) {
	m := ManagementService{
		log: &noopLogger,
		logger: &Logger{
			Log: &noopLogger,
		},
	}
	actor := actor{ID: "test"}
	_, cancel := context.WithCancel(context.Background())
	session1 := newSession(0, actor, cancel)
	assert.True(t, m.canStartStream(session1))
	m.logger.Listen(session1)
	assert.True(t, session1.Active())

	// Try another session
	session2 := newSession(0, actor, cancel)
	assert.Equal(t, 1, m.logger.ActiveSessions())
	assert.True(t, m.canStartStream(session2))
	// session1 is removed and stopped
	assert.Equal(t, 0, m.logger.ActiveSessions())
	assert.False(t, session1.Active())
}