package botlink

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"scrabble/pkg/mtls"
	botlinkv1 "scrabble/pkg/proto/botlink/v1"
)

// mintCerts writes a CA, a server leaf (IP SAN 127.0.0.1) and a client leaf into a
// temp dir, returning their file paths. It exercises the real pkg/mtls loaders.
func mintCerts(t *testing.T) (caFile, srvCert, srvKey, cliCert, cliKey string) {
	t.Helper()
	dir := t.TempDir()

	caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	caTmpl := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               pkix.Name{CommonName: "test-ca"},
		NotBefore:             time.Now().Add(-time.Hour),
		NotAfter:              time.Now().Add(time.Hour),
		IsCA:                  true,
		KeyUsage:              x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}
	caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
	if err != nil {
		t.Fatalf("ca: %v", err)
	}
	caCert, _ := x509.ParseCertificate(caDER)

	leaf := func(cn string, eku x509.ExtKeyUsage, ips []net.IP) (certPath, keyPath string) {
		key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		tmpl := &x509.Certificate{
			SerialNumber: big.NewInt(time.Now().UnixNano()),
			Subject:      pkix.Name{CommonName: cn},
			NotBefore:    time.Now().Add(-time.Hour),
			NotAfter:     time.Now().Add(time.Hour),
			KeyUsage:     x509.KeyUsageDigitalSignature,
			ExtKeyUsage:  []x509.ExtKeyUsage{eku},
			IPAddresses:  ips,
		}
		der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
		if err != nil {
			t.Fatalf("leaf %s: %v", cn, err)
		}
		certPath = filepath.Join(dir, cn+".crt")
		keyPath = filepath.Join(dir, cn+".key")
		writePEM(t, certPath, "CERTIFICATE", der)
		keyDER, _ := x509.MarshalPKCS8PrivateKey(key)
		writePEM(t, keyPath, "PRIVATE KEY", keyDER)
		return certPath, keyPath
	}

	caFile = filepath.Join(dir, "ca.crt")
	writePEM(t, caFile, "CERTIFICATE", caDER)
	srvCert, srvKey = leaf("server", x509.ExtKeyUsageServerAuth, []net.IP{net.ParseIP("127.0.0.1")})
	cliCert, cliKey = leaf("client", x509.ExtKeyUsageClientAuth, nil)
	return caFile, srvCert, srvKey, cliCert, cliKey
}

func writePEM(t *testing.T, path, typ string, der []byte) {
	t.Helper()
	if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: typ, Bytes: der}), 0o600); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

// startMTLSHub starts the Hub behind a real mTLS gRPC listener and returns its
// address and the CA/client cert paths.
func startMTLSHub(t *testing.T) (hub *Hub, addr, caFile, cliCert, cliKey string) {
	t.Helper()
	caFile, srvCert, srvKey, cliCert, cliKey := mintCerts(t)
	tlsCfg, err := mtls.ServerConfig(srvCert, srvKey, caFile)
	if err != nil {
		t.Fatalf("server config: %v", err)
	}
	lis, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	hub = NewHub(nil, nil, nil)
	srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsCfg)))
	botlinkv1.RegisterBotLinkServer(srv, hub)
	go func() { _ = srv.Serve(lis) }()
	t.Cleanup(srv.Stop)
	return hub, lis.Addr().String(), caFile, cliCert, cliKey
}

// TestMTLSValidClientDelivers verifies a CA-signed bot connects and receives a
// pushed command.
func TestMTLSValidClientDelivers(t *testing.T) {
	hub, addr, caFile, cliCert, cliKey := startMTLSHub(t)
	tlsCfg, err := mtls.ClientConfig(cliCert, cliKey, caFile, "127.0.0.1")
	if err != nil {
		t.Fatalf("client config: %v", err)
	}
	conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	ctx := t.Context()
	bot := &fakeBot{delivered: true, ack: true}
	bot.connect(t, ctx, hub, botlinkv1.NewBotLinkClient(conn))

	delivered, err := hub.SendAwait(ctx, SendToUserCommand("42", "hi"))
	if err != nil || !delivered {
		t.Fatalf("SendAwait = (%v, %v), want (true, nil)", delivered, err)
	}
}

// TestMTLSRejectsClientWithoutCert verifies the gateway refuses a client that
// presents no CA-signed certificate — mTLS is the sole guard on the bot-link.
func TestMTLSRejectsClientWithoutCert(t *testing.T) {
	hub, addr, caFile, _, _ := startMTLSHub(t)
	caPEM, _ := os.ReadFile(caFile)
	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM(caPEM)
	// Trusts the server, but presents no client certificate.
	noCert := credentials.NewTLS(&tls.Config{RootCAs: pool, ServerName: "127.0.0.1", MinVersion: tls.VersionTLS13})
	conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(noCert))
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	stream, err := botlinkv1.NewBotLinkClient(conn).Link(t.Context())
	if err == nil {
		err = stream.Send(&botlinkv1.FromBot{Msg: &botlinkv1.FromBot_Hello{Hello: &botlinkv1.Hello{InstanceId: "rogue"}}})
		if err == nil {
			_, err = stream.Recv()
		}
	}
	if err == nil {
		t.Fatal("expected the handshake to be rejected without a client certificate")
	}
	hub.mu.Lock()
	n := len(hub.links)
	hub.mu.Unlock()
	if n != 0 {
		t.Errorf("connected bots = %d, want 0 (rogue must not register)", n)
	}
}
