package gssapi

import (
	"encoding/binary"
	"encoding/hex"
	"testing"

	"github.com/jcmturner/gokrb5/v8/iana/keyusage"
	"github.com/jcmturner/gokrb5/v8/types"
	"github.com/stretchr/testify/assert"
)

const (
	testMICPayload = "deadbeef"
	// What a kerberized server might send
	testMICChallengeFromAcceptor = "040401ffffffffff00000000575e85d6c34d12ba3e5b1b1310cd9cb3"
	// What an initiator client could reply
	testMICChallengeReplyFromInitiator = "040400ffffffffff00000000000000009649ca09d2f1bc51ff6e5ca3"

	acceptorSign  = keyusage.GSSAPI_ACCEPTOR_SIGN
	initiatorSign = keyusage.GSSAPI_INITIATOR_SIGN
)

func getMICChallengeReference() *MICToken {
	challenge, _ := hex.DecodeString(testMICChallengeFromAcceptor)
	return &MICToken{
		Flags:     MICTokenFlagSentByAcceptor,
		SndSeqNum: binary.BigEndian.Uint64(challenge[8:16]),
		Payload:   nil,
		Checksum:  challenge[16:],
	}
}

func getMICChallengeReferenceNoChksum() *MICToken {
	c := getMICChallengeReference()
	c.Checksum = nil
	return c
}

func getMICResponseReference() *MICToken {
	response, _ := hex.DecodeString(testMICChallengeReplyFromInitiator)
	return &MICToken{
		Flags:     0x00,
		SndSeqNum: 0,
		Payload:   nil,
		Checksum:  response[16:],
	}
}

func getMICResponseReferenceNoChkSum() *MICToken {
	r := getMICResponseReference()
	r.Checksum = nil
	return r
}

func TestUnmarshal_MICChallenge(t *testing.T) {
	t.Parallel()
	challenge, _ := hex.DecodeString(testMICChallengeFromAcceptor)
	var mt MICToken
	err := mt.Unmarshal(challenge, true)
	assert.Nil(t, err, "Unexpected error occurred.")
	assert.Equal(t, getMICChallengeReference(), &mt, "Token not decoded as expected.")
}

func TestUnmarshalFailure_MICChallenge(t *testing.T) {
	t.Parallel()
	challenge, _ := hex.DecodeString(testMICChallengeFromAcceptor)
	var mt MICToken
	err := mt.Unmarshal(challenge, false)
	assert.NotNil(t, err, "Expected error did not occur: a message from the acceptor cannot be expected to be sent from the initiator.")
	assert.Nil(t, mt.Payload, "Token fields should not have been initialised")
	assert.Nil(t, mt.Checksum, "Token fields should not have been initialised")
	assert.Equal(t, byte(0x00), mt.Flags, "Token fields should not have been initialised")
	assert.Equal(t, uint64(0), mt.SndSeqNum, "Token fields should not have been initialised")
}

func TestUnmarshal_MICChallengeReply(t *testing.T) {
	t.Parallel()
	response, _ := hex.DecodeString(testMICChallengeReplyFromInitiator)
	var mt MICToken
	err := mt.Unmarshal(response, false)
	assert.Nil(t, err, "Unexpected error occurred.")
	assert.Equal(t, getMICResponseReference(), &mt, "Token not decoded as expected.")
}

func TestUnmarshalFailure_MICChallengeReply(t *testing.T) {
	t.Parallel()
	response, _ := hex.DecodeString(testMICChallengeReplyFromInitiator)
	var mt MICToken
	err := mt.Unmarshal(response, true)
	assert.NotNil(t, err, "Expected error did not occur: a message from the initiator cannot be expected to be sent from the acceptor.")
	assert.Nil(t, mt.Payload, "Token fields should not have been initialised")
	assert.Nil(t, mt.Checksum, "Token fields should not have been initialised")
	assert.Equal(t, byte(0x00), mt.Flags, "Token fields should not have been initialised")
	assert.Equal(t, uint64(0), mt.SndSeqNum, "Token fields should not have been initialised")
}

func TestMICChallengeChecksumVerification(t *testing.T) {
	t.Parallel()
	challenge, _ := hex.DecodeString(testMICChallengeFromAcceptor)
	var mt MICToken
	mt.Unmarshal(challenge, true)
	mt.Payload, _ = hex.DecodeString(testMICPayload)
	challengeOk, cErr := mt.Verify(getSessionKey(), acceptorSign)
	assert.Nil(t, cErr, "Error occurred during checksum verification.")
	assert.True(t, challengeOk, "Checksum verification failed.")
}

func TestMICResponseChecksumVerification(t *testing.T) {
	t.Parallel()
	reply, _ := hex.DecodeString(testMICChallengeReplyFromInitiator)
	var mt MICToken
	mt.Unmarshal(reply, false)
	mt.Payload, _ = hex.DecodeString(testMICPayload)
	replyOk, rErr := mt.Verify(getSessionKey(), initiatorSign)
	assert.Nil(t, rErr, "Error occurred during checksum verification.")
	assert.True(t, replyOk, "Checksum verification failed.")
}

func TestMICChecksumVerificationFailure(t *testing.T) {
	t.Parallel()
	challenge, _ := hex.DecodeString(testMICChallengeFromAcceptor)
	var mt MICToken
	mt.Unmarshal(challenge, true)

	// Test a failure with the correct key but wrong keyusage:
	challengeOk, cErr := mt.Verify(getSessionKey(), initiatorSign)
	assert.NotNil(t, cErr, "Expected error did not occur.")
	assert.False(t, challengeOk, "Checksum verification succeeded when it should have failed.")

	wrongKeyVal, _ := hex.DecodeString("14f9bde6b50ec508201a97f74c4effff")
	badKey := types.EncryptionKey{
		KeyType:  sessionKeyType,
		KeyValue: wrongKeyVal,
	}
	// Test a failure with the wrong key but correct keyusage:
	wrongKeyOk, wkErr := mt.Verify(badKey, acceptorSign)
	assert.NotNil(t, wkErr, "Expected error did not occur.")
	assert.False(t, wrongKeyOk, "Checksum verification succeeded when it should have failed.")
}

func TestMarshal_MICChallenge(t *testing.T) {
	t.Parallel()
	bytes, _ := getMICChallengeReference().Marshal()
	assert.Equal(t, testMICChallengeFromAcceptor, hex.EncodeToString(bytes),
		"Marshalling did not yield the expected result.")
}

func TestMarshal_MICChallengeReply(t *testing.T) {
	t.Parallel()
	bytes, _ := getMICResponseReference().Marshal()
	assert.Equal(t, testMICChallengeReplyFromInitiator, hex.EncodeToString(bytes),
		"Marshalling did not yield the expected result.")
}

func TestMarshal_MICFailures(t *testing.T) {
	t.Parallel()
	noChkSum := getMICResponseReferenceNoChkSum()
	chkBytes, chkErr := noChkSum.Marshal()
	assert.Nil(t, chkBytes, "No bytes should be returned.")
	assert.NotNil(t, chkErr, "Expected an error as no checksum was set")
}

func TestNewInitiatorMICTokenSignatureAndMarshalling(t *testing.T) {
	t.Parallel()
	bytes, _ := hex.DecodeString(testMICPayload)
	token, tErr := NewInitiatorMICToken(bytes, getSessionKey())
	token.Payload = nil
	assert.Nil(t, tErr, "Unexpected error.")
	assert.Equal(t, getMICResponseReference(), token, "Token failed to be marshalled to the expected bytes.")
}
