package main

import (
	"context"
	"crypto/ed25519"
	"crypto/sha256"
	"crypto/x509"
	"encoding/pem"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"galaxy/gateway/internal/backendclient"
	"galaxy/gateway/internal/config"
	"galaxy/redisconn"

	"github.com/alicebob/miniredis/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

func testRedisConn(masterAddr string, opTimeout time.Duration) redisconn.Config {
	cfg := redisconn.DefaultConfig()
	cfg.MasterAddr = masterAddr
	cfg.Password = "integration"
	if opTimeout > 0 {
		cfg.OperationTimeout = opTimeout
	}
	return cfg
}

func newTestBackendConfig() config.BackendConfig {
	return config.BackendConfig{
		HTTPBaseURL:              "http://127.0.0.1:8080",
		GRPCPushURL:              "127.0.0.1:8081",
		GatewayClientID:          "gw-test",
		HTTPTimeout:              250 * time.Millisecond,
		PushReconnectBaseBackoff: 100 * time.Millisecond,
		PushReconnectMaxBackoff:  time.Second,
	}
}

func newTestBackendClient(t *testing.T) *backendclient.Client {
	t.Helper()
	cfg := newTestBackendConfig()
	client, err := backendclient.NewClient(backendclient.Config{
		HTTPBaseURL:              cfg.HTTPBaseURL,
		GRPCPushURL:              cfg.GRPCPushURL,
		GatewayClientID:          cfg.GatewayClientID,
		HTTPTimeout:              cfg.HTTPTimeout,
		PushReconnectBaseBackoff: cfg.PushReconnectBaseBackoff,
		PushReconnectMaxBackoff:  cfg.PushReconnectMaxBackoff,
	})
	require.NoError(t, err)
	t.Cleanup(func() { _ = client.Close() })
	return client
}

func TestNewAuthenticatedGRPCDependenciesSuccess(t *testing.T) {
	t.Parallel()

	server := miniredis.RunT(t)
	responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
	backend := newTestBackendClient(t)

	cfg := config.Config{
		Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
		ReplayRedis: config.ReplayRedisConfig{
			KeyPrefix:      "gateway:replay:",
			ReserveTimeout: 250 * time.Millisecond,
		},
		Backend: newTestBackendConfig(),
		ResponseSigner: config.ResponseSignerConfig{
			PrivateKeyPEMPath: responseSignerPEMPath,
		},
	}

	deps, components, cleanup, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
	require.NoError(t, err)
	require.NotNil(t, deps.SessionCache)
	require.NotNil(t, deps.ReplayStore)
	require.NotNil(t, deps.ResponseSigner)
	require.NotNil(t, deps.Router)
	require.NotNil(t, deps.Service)
	require.Len(t, components, 1)
	require.NotNil(t, cleanup)
	assert.NoError(t, cleanup())
}

func TestNewAuthenticatedGRPCDependenciesPingFailure(t *testing.T) {
	t.Parallel()

	responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
	backend := newTestBackendClient(t)

	cfg := config.Config{
		Redis: testRedisConn(unusedTCPAddr(t), 100*time.Millisecond),
		ReplayRedis: config.ReplayRedisConfig{
			KeyPrefix:      "gateway:replay:",
			ReserveTimeout: 100 * time.Millisecond,
		},
		Backend: newTestBackendConfig(),
		ResponseSigner: config.ResponseSignerConfig{
			PrivateKeyPEMPath: responseSignerPEMPath,
		},
	}

	_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
	require.Error(t, err)
	assert.ErrorContains(t, err, "ping redis")
}

func TestNewAuthenticatedGRPCDependenciesInvalidReplayConfig(t *testing.T) {
	t.Parallel()

	server := miniredis.RunT(t)
	responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
	backend := newTestBackendClient(t)

	cfg := config.Config{
		Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
		ReplayRedis: config.ReplayRedisConfig{
			ReserveTimeout: 250 * time.Millisecond,
		},
		Backend: newTestBackendConfig(),
		ResponseSigner: config.ResponseSignerConfig{
			PrivateKeyPEMPath: responseSignerPEMPath,
		},
	}

	_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
	require.Error(t, err)
	assert.ErrorContains(t, err, "replay key prefix must not be empty")
}

func TestNewAuthenticatedGRPCDependenciesMissingResponseSigner(t *testing.T) {
	t.Parallel()

	backend := newTestBackendClient(t)

	cfg := config.Config{
		Backend: newTestBackendConfig(),
	}

	_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
	require.Error(t, err)
	assert.ErrorContains(t, err, "load response signer")
}

func TestNewAuthenticatedGRPCDependenciesInvalidResponseSignerPEM(t *testing.T) {
	t.Parallel()

	backend := newTestBackendClient(t)
	server := miniredis.RunT(t)

	cfg := config.Config{
		Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
		ReplayRedis: config.ReplayRedisConfig{
			KeyPrefix:      "gateway:replay:",
			ReserveTimeout: 250 * time.Millisecond,
		},
		Backend: newTestBackendConfig(),
		ResponseSigner: config.ResponseSignerConfig{
			PrivateKeyPEMPath: writeInvalidPEMFile(t),
		},
	}

	_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
	require.Error(t, err)
	assert.ErrorContains(t, err, "response signer private key")
}

func unusedTCPAddr(t *testing.T) string {
	t.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)

	addr := listener.Addr().String()
	require.NoError(t, listener.Close())

	return addr
}

func writeTestResponseSignerPEMFile(t *testing.T) string {
	t.Helper()

	seed := sha256.Sum256([]byte("gateway-main-test-response-signer"))
	privateKey := ed25519.NewKeyFromSeed(seed[:])

	encodedPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
	require.NoError(t, err)

	path := filepath.Join(t.TempDir(), "response-signer.pem")
	err = os.WriteFile(path, pem.EncodeToMemory(&pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: encodedPrivateKey,
	}), 0o600)
	require.NoError(t, err)

	return path
}

func writeInvalidPEMFile(t *testing.T) string {
	t.Helper()

	path := filepath.Join(t.TempDir(), "invalid-response-signer.pem")
	require.NoError(t, os.WriteFile(path, []byte("not a valid pem"), 0o600))

	return path
}
