package authn_test

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"testing"

	"galaxy/core/canon"
	"galaxy/core/keypair"
	"galaxy/gateway/authn"

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

func sha256Of(payload []byte) []byte {
	sum := sha256.Sum256(payload)
	return sum[:]
}

// TestParityWithUICoreCanonicalBytes proves that the gateway-side
// authn package and the client-side ui/core canon package produce the
// exact same canonical signing input for every v1 envelope. Any drift
// here means a client signature would be silently rejected by the
// gateway (or vice versa).
func TestParityWithUICoreCanonicalBytes(t *testing.T) {
	t.Parallel()

	t.Run("request", func(t *testing.T) {
		t.Parallel()

		gatewayFields := authn.RequestSigningFields{
			ProtocolVersion: "v1",
			DeviceSessionID: "device-session-parity",
			MessageType:     "user.games.command",
			TimestampMS:     1_700_000_000_000,
			RequestID:       "request-parity",
			PayloadHash:     sha256Of([]byte("payload")),
		}
		clientFields := canon.RequestSigningFields{
			ProtocolVersion: gatewayFields.ProtocolVersion,
			DeviceSessionID: gatewayFields.DeviceSessionID,
			MessageType:     gatewayFields.MessageType,
			TimestampMS:     gatewayFields.TimestampMS,
			RequestID:       gatewayFields.RequestID,
			PayloadHash:     gatewayFields.PayloadHash,
		}

		assert.Equal(t,
			authn.BuildRequestSigningInput(gatewayFields),
			canon.BuildRequestSigningInput(clientFields))
	})

	t.Run("response", func(t *testing.T) {
		t.Parallel()

		gatewayFields := authn.ResponseSigningFields{
			ProtocolVersion: "v1",
			RequestID:       "request-parity",
			TimestampMS:     1_700_000_000_500,
			ResultCode:      "ok",
			PayloadHash:     sha256Of([]byte("response-payload")),
		}
		clientFields := canon.ResponseSigningFields{
			ProtocolVersion: gatewayFields.ProtocolVersion,
			RequestID:       gatewayFields.RequestID,
			TimestampMS:     gatewayFields.TimestampMS,
			ResultCode:      gatewayFields.ResultCode,
			PayloadHash:     gatewayFields.PayloadHash,
		}

		assert.Equal(t,
			authn.BuildResponseSigningInput(gatewayFields),
			canon.BuildResponseSigningInput(clientFields))
	})

	t.Run("event", func(t *testing.T) {
		t.Parallel()

		gatewayFields := authn.EventSigningFields{
			EventType:   "gateway.server_time",
			EventID:     "evt-parity",
			TimestampMS: 1_700_000_001_000,
			RequestID:   "request-parity",
			TraceID:     "trace-parity",
			PayloadHash: sha256Of([]byte("event-payload")),
		}
		clientFields := canon.EventSigningFields{
			EventType:   gatewayFields.EventType,
			EventID:     gatewayFields.EventID,
			TimestampMS: gatewayFields.TimestampMS,
			RequestID:   gatewayFields.RequestID,
			TraceID:     gatewayFields.TraceID,
			PayloadHash: gatewayFields.PayloadHash,
		}

		assert.Equal(t,
			authn.BuildEventSigningInput(gatewayFields),
			canon.BuildEventSigningInput(clientFields))
	})
}

// TestParityRequestSignedByUICoreAcceptedByGateway proves that a
// request the client signs with `keypair.Sign` is accepted by the
// gateway's `authn.VerifyRequestSignature`. This is the acceptance
// criterion from `ui/PLAN.md` Phase 3.
func TestParityRequestSignedByUICoreAcceptedByGateway(t *testing.T) {
	t.Parallel()

	privateKey, publicKey, err := keypair.Generate(rand.Reader)
	require.NoError(t, err)

	clientFields := canon.RequestSigningFields{
		ProtocolVersion: "v1",
		DeviceSessionID: "device-session-parity",
		MessageType:     "user.account.get",
		TimestampMS:     1_700_000_000_000,
		RequestID:       "request-parity",
		PayloadHash:     sha256Of([]byte("payload")),
	}
	signature, err := keypair.Sign(privateKey, canon.BuildRequestSigningInput(clientFields))
	require.NoError(t, err)

	encodedKey, err := keypair.MarshalPublicKey(publicKey)
	require.NoError(t, err)

	gatewayFields := authn.RequestSigningFields{
		ProtocolVersion: clientFields.ProtocolVersion,
		DeviceSessionID: clientFields.DeviceSessionID,
		MessageType:     clientFields.MessageType,
		TimestampMS:     clientFields.TimestampMS,
		RequestID:       clientFields.RequestID,
		PayloadHash:     clientFields.PayloadHash,
	}

	require.NoError(t,
		authn.VerifyRequestSignature(encodedKey, signature, gatewayFields))
}

// TestParityResponseSignedByGatewayAcceptedByUICore proves that a
// response signed by the gateway's `Ed25519ResponseSigner` is
// accepted by the client's `canon.VerifyResponseSignature`. The
// reverse acceptance criterion from `ui/PLAN.md` Phase 3.
func TestParityResponseSignedByGatewayAcceptedByUICore(t *testing.T) {
	t.Parallel()

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	signer, err := authn.NewEd25519ResponseSigner(privateKey)
	require.NoError(t, err)

	gatewayFields := authn.ResponseSigningFields{
		ProtocolVersion: "v1",
		RequestID:       "request-parity",
		TimestampMS:     1_700_000_000_500,
		ResultCode:      "ok",
		PayloadHash:     sha256Of([]byte("response-payload")),
	}
	signature, err := signer.SignResponse(gatewayFields)
	require.NoError(t, err)

	clientFields := canon.ResponseSigningFields{
		ProtocolVersion: gatewayFields.ProtocolVersion,
		RequestID:       gatewayFields.RequestID,
		TimestampMS:     gatewayFields.TimestampMS,
		ResultCode:      gatewayFields.ResultCode,
		PayloadHash:     gatewayFields.PayloadHash,
	}

	require.NoError(t,
		canon.VerifyResponseSignature(signer.PublicKey(), signature, clientFields))
}

// TestParityEventSignedByGatewayAcceptedByUICore proves that a
// stream event signed by the gateway's response signer (which signs
// both responses and events with the same key) is accepted by the
// client's `canon.VerifyEventSignature`.
func TestParityEventSignedByGatewayAcceptedByUICore(t *testing.T) {
	t.Parallel()

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	signer, err := authn.NewEd25519ResponseSigner(privateKey)
	require.NoError(t, err)

	gatewayFields := authn.EventSigningFields{
		EventType:   "gateway.server_time",
		EventID:     "evt-parity",
		TimestampMS: 1_700_000_001_000,
		RequestID:   "request-parity",
		TraceID:     "trace-parity",
		PayloadHash: sha256Of([]byte("event-payload")),
	}
	signature, err := signer.SignEvent(gatewayFields)
	require.NoError(t, err)

	clientFields := canon.EventSigningFields{
		EventType:   gatewayFields.EventType,
		EventID:     gatewayFields.EventID,
		TimestampMS: gatewayFields.TimestampMS,
		RequestID:   gatewayFields.RequestID,
		TraceID:     gatewayFields.TraceID,
		PayloadHash: gatewayFields.PayloadHash,
	}

	require.NoError(t,
		canon.VerifyEventSignature(signer.PublicKey(), signature, clientFields))
}

// TestParityClientPublicKeyEncodingMatchesBackend proves that the
// base64 encoding `keypair.MarshalPublicKey` produces is the exact
// string form `authn.VerifyRequestSignature` expects when the
// gateway reads a client public key out of session cache.
func TestParityClientPublicKeyEncodingMatchesBackend(t *testing.T) {
	t.Parallel()

	_, publicKey, err := keypair.Generate(rand.Reader)
	require.NoError(t, err)

	encoded, err := keypair.MarshalPublicKey(publicKey)
	require.NoError(t, err)

	expected := base64.StdEncoding.EncodeToString(publicKey)
	require.Equal(t, expected, encoded)
}
