package server

import (
	"context"
	"errors"
	"net/http/httptest"
	"testing"
	"time"

	"code.justin.tv/live/autohost/internal/metrics"

	"code.justin.tv/live/autohost/internal/hosting/clients/liveline"

	"github.com/twitchtv/twirp"

	"code.justin.tv/creator-collab/log"
	"code.justin.tv/live/autohost/internal/hosting/auth"
	"code.justin.tv/live/autohost/internal/hosting/clients/clue"
	"code.justin.tv/live/autohost/internal/hosting/clients/recs"
	"code.justin.tv/live/autohost/internal/hosting/clients/sns"
	"code.justin.tv/live/autohost/internal/hosting/clients/spade"
	"code.justin.tv/live/autohost/internal/hosting/config"
	"code.justin.tv/live/autohost/internal/hosting/eventbus"
	"code.justin.tv/live/autohost/internal/hosting/logic"
	"code.justin.tv/live/autohost/internal/hosting/memcached"
	"code.justin.tv/live/autohost/internal/hosting/pdms"
	"code.justin.tv/live/autohost/internal/hosting/storage"
	"code.justin.tv/live/autohost/internal/localdynamo"
	"code.justin.tv/live/autohost/rpc/hosting"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
	"github.com/stretchr/testify/require"
)

type testEnv struct {
	httpServer    *httptest.Server
	hostingClient hosting.Hosting

	logic          logic.Logic
	localTables    *localdynamo.Tables
	dynamoDBClient dynamodbiface.DynamoDBAPI
	storageClient  storage.Storage

	// Stubs that tests will configure.
	authStub      *auth.Stub
	memcachedStub memcached.Cache
	recsStub      *recs.StubClient
	spadeStub     *spade.StubClient

	// Mocks that tests can configure
	clueMock     *clue.MockClient
	pdmsMock     *pdms.MockClient
	livelineMock *liveline.MockClient
}

func newTestEnv(t *testing.T) *testEnv {
	conf, err := config.GetConfig()
	require.NoError(t, err)
	require.True(t, conf.Environment == config.EnvCI || conf.Environment == config.EnvDev)
	require.NotNil(t, conf)
	require.NotEmpty(t, conf.DynamoDB.LocalDynamoEndpoint)
	require.NotEmpty(t, conf.DynamoDB.HostTableName)
	require.NotEmpty(t, conf.DynamoDB.SettingsTableName)

	logger := log.NewDevelopmentLogger()

	localTables, err := localdynamo.NewLocalDynamoTables(conf.DynamoDB.LocalDynamoEndpoint, logger)
	require.NoError(t, err)

	localDynamoClient, err := localdynamo.NewLocalDynamoDBClient(conf.DynamoDB.LocalDynamoEndpoint)
	require.NoError(t, err)

	storage := storage.New(&storage.Params{
		Client:            localDynamoClient,
		Logger:            logger,
		HostTableName:     conf.DynamoDB.HostTableName,
		SettingsTableName: conf.DynamoDB.SettingsTableName,
	})

	memcached, err := memcached.NewCache(conf.Memcached)
	require.NoError(t, err)

	authStub := auth.NewAuthStub()
	spadeStub := spade.NewStubClient()
	recsStub := recs.NewStubClient()
	clueStub := &clue.MockClient{}
	pdmsClient := &pdms.MockClient{}
	livelineClient := &liveline.MockClient{}

	sampleReporter := metrics.NewSampleReporter(&metrics.SampleReporterConfig{
		Environment:             conf.Environment,
		Logger:                  logger,
		Region:                  "us-west-2",
		RunningInEC2:            conf.RunningInEC2,
		SendMetricsToCloudwatch: conf.EnableCloudWatchMetrics,
		ServiceName:             "Autohost Server",
	})

	hostingLogic, err := logic.New(&logic.Params{
		DB:               storage,
		SNSClient:        sns.NewNoopClient(),
		Auth:             authStub,
		ClueClient:       clueStub,
		SpadeClient:      spadeStub,
		EventPublisher:   eventbus.NewNoopPublisher(),
		Memcached:        memcached,
		PDMSClient:       pdmsClient,
		TwitchRecsClient: recsStub,
		Logger:           logger,
		LivelineClient:   livelineClient,
		SampleReporter:   sampleReporter,
	})

	// Start an HTTP server to host the raids twirp handlers.
	twirpHandlers := NewTwirpHandlers(hostingLogic)
	hostingTwirpHandler := hosting.NewHostingServer(twirpHandlers, nil)
	httpServer := httptest.NewServer(hostingTwirpHandler)

	// Create a client configured to send requests to the HTTP server.
	hostingClient := hosting.NewHostingProtobufClient(httpServer.URL, httpServer.Client())

	// In case memcached is using a live client, flush and close
	t.Cleanup(func() {
		memcached.FlushAll()
		memcached.Close()
		localTables.Reset()
		httpServer.Close()
	})

	return &testEnv{
		httpServer:    httpServer,
		hostingClient: hostingClient,

		logic:          hostingLogic,
		localTables:    localTables,
		dynamoDBClient: localDynamoClient,

		authStub:      authStub,
		memcachedStub: memcached,
		recsStub:      recsStub,
		storageClient: storage,

		livelineMock: livelineClient,
		clueMock:     clueStub,
		pdmsMock:     pdmsClient,
		spadeStub:    spadeStub,
	}
}

type testEnvWithMockLogic struct {
	httpServer *httptest.Server
	client     hosting.Hosting
	mockLogic  *logic.MockLogic
}

func newTestEnvWithMockLogic(t *testing.T) *testEnvWithMockLogic {
	mockLogic := &logic.MockLogic{}

	// Start an HTTP server to host the hosting twirp handlers.
	twirpHandlers := NewTwirpHandlers(mockLogic)
	hostingTwirpHandler := hosting.NewHostingServer(twirpHandlers, nil)
	httpServer := httptest.NewServer(hostingTwirpHandler)

	// Create a client configured to send requests to the HTTP server.
	hostingClient := hosting.NewHostingProtobufClient(httpServer.URL, httpServer.Client())

	// Close the HTTP server when the test ends.
	t.Cleanup(func() {
		httpServer.Close()
	})

	return &testEnvWithMockLogic{
		httpServer: httpServer,
		client:     hostingClient,
		mockLogic:  mockLogic,
	}
}

func requireTwirpError(t *testing.T, expectedErrorCode twirp.ErrorCode, actualErr error) {
	require.Error(t, actualErr)

	var twirpErr twirp.Error
	isTwirpError := errors.As(actualErr, &twirpErr)
	if !isTwirpError {
		t.Fatalf("error %v was expected to be of type *twirp.Error, but was %T", actualErr, actualErr)
	}

	require.Equal(t, expectedErrorCode, twirpErr.Code())
}

func getContextWithTimeout() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), 30*time.Second)
}
