package domain_test

import (
	"testing"
	"time"

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

	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
)

func TestNewMPCSession(t *testing.T) {
	t.Run("should create keygen session successfully", func(t *testing.T) {
		threshold, err := value_objects.NewThreshold(2, 3)
		require.NoError(t, err)

		session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "user123", 10*time.Minute, nil)
		require.NoError(t, err)

		assert.NotNil(t, session)
		assert.False(t, session.ID.IsZero())
		assert.Equal(t, entities.SessionTypeKeygen, session.SessionType)
		assert.Equal(t, 2, session.Threshold.T())
		assert.Equal(t, 3, session.Threshold.N())
		assert.Equal(t, value_objects.SessionStatusCreated, session.Status)
		assert.Equal(t, "user123", session.CreatedBy)
		assert.True(t, session.ExpiresAt.After(time.Now()))
	})

	t.Run("should create sign session successfully", func(t *testing.T) {
		threshold, err := value_objects.NewThreshold(2, 3)
		require.NoError(t, err)

		messageHash := []byte("test-message-hash")
		session, err := entities.NewMPCSession(entities.SessionTypeSign, threshold, "user456", 10*time.Minute, messageHash)
		require.NoError(t, err)

		assert.Equal(t, entities.SessionTypeSign, session.SessionType)
		assert.Equal(t, messageHash, session.MessageHash)
	})

	t.Run("should fail sign session without message hash", func(t *testing.T) {
		threshold, err := value_objects.NewThreshold(2, 3)
		require.NoError(t, err)

		_, err = entities.NewMPCSession(entities.SessionTypeSign, threshold, "user456", 10*time.Minute, nil)
		assert.Error(t, err)
	})
}

func TestMPCSession_AddParticipant(t *testing.T) {
	t.Run("should add participant successfully", func(t *testing.T) {
		threshold, _ := value_objects.NewThreshold(2, 3)
		session, _ := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "user123", 10*time.Minute, nil)

		partyID, _ := value_objects.NewPartyID("party1")
		participant, err := entities.NewParticipant(partyID, 0, entities.DeviceInfo{
			DeviceType: entities.DeviceTypeIOS,
			DeviceID:   "device1",
			Platform:   "ios",
			AppVersion: "1.0.0",
		})
		require.NoError(t, err)

		err = session.AddParticipant(participant)
		require.NoError(t, err)
		assert.Len(t, session.Participants, 1)
	})

	t.Run("should fail when participant limit reached", func(t *testing.T) {
		threshold, _ := value_objects.NewThreshold(2, 2)
		session, _ := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "user123", 10*time.Minute, nil)

		// Add max participants
		for i := 0; i < 2; i++ {
			partyID, _ := value_objects.NewPartyID(string(rune('a' + i)))
			participant, _ := entities.NewParticipant(partyID, i, entities.DeviceInfo{
				DeviceType: entities.DeviceTypeIOS,
				DeviceID:   "device",
				Platform:   "ios",
				AppVersion: "1.0.0",
			})
			err := session.AddParticipant(participant)
			require.NoError(t, err)
		}

		// Try to add one more
		extraPartyID, _ := value_objects.NewPartyID("extra")
		extraParticipant, _ := entities.NewParticipant(extraPartyID, 2, entities.DeviceInfo{
			DeviceType: entities.DeviceTypeIOS,
			DeviceID:   "device",
			Platform:   "ios",
			AppVersion: "1.0.0",
		})
		err := session.AddParticipant(extraParticipant)
		assert.Error(t, err)
	})
}

func TestMPCSession_IsExpired(t *testing.T) {
	t.Run("should return true for expired session", func(t *testing.T) {
		threshold, _ := value_objects.NewThreshold(2, 3)
		session, _ := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "user123", 10*time.Minute, nil)
		session.ExpiresAt = time.Now().Add(-1 * time.Hour)

		assert.True(t, session.IsExpired())
	})

	t.Run("should return false for active session", func(t *testing.T) {
		threshold, _ := value_objects.NewThreshold(2, 3)
		session, _ := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "user123", 10*time.Minute, nil)

		assert.False(t, session.IsExpired())
	})
}

func TestThreshold(t *testing.T) {
	t.Run("should create valid threshold", func(t *testing.T) {
		threshold, err := value_objects.NewThreshold(2, 3)
		require.NoError(t, err)

		assert.Equal(t, 2, threshold.T())
		assert.Equal(t, 3, threshold.N())
		assert.False(t, threshold.IsZero())
	})

	t.Run("should fail with t greater than n", func(t *testing.T) {
		_, err := value_objects.NewThreshold(4, 3)
		assert.Error(t, err)
	})

	t.Run("should fail with t less than 1", func(t *testing.T) {
		_, err := value_objects.NewThreshold(0, 3)
		assert.Error(t, err)
	})

	t.Run("should fail with n less than 2", func(t *testing.T) {
		_, err := value_objects.NewThreshold(1, 1)
		assert.Error(t, err)
	})
}

func TestParticipant(t *testing.T) {
	t.Run("should create participant with correct initial state", func(t *testing.T) {
		partyID, _ := value_objects.NewPartyID("party1")
		participant, err := entities.NewParticipant(partyID, 0, entities.DeviceInfo{
			DeviceType: entities.DeviceTypeIOS,
			DeviceID:   "device1",
			Platform:   "ios",
			AppVersion: "1.0.0",
		})
		require.NoError(t, err)

		assert.Equal(t, partyID, participant.PartyID)
		assert.Equal(t, 0, participant.PartyIndex)
		assert.Equal(t, value_objects.ParticipantStatusInvited, participant.Status)
	})

	t.Run("should transition states correctly", func(t *testing.T) {
		partyID, _ := value_objects.NewPartyID("party1")
		participant, _ := entities.NewParticipant(partyID, 0, entities.DeviceInfo{
			DeviceType: entities.DeviceTypeIOS,
			DeviceID:   "device1",
			Platform:   "ios",
			AppVersion: "1.0.0",
		})

		// Invited -> Joined
		err := participant.Join()
		require.NoError(t, err)
		assert.Equal(t, value_objects.ParticipantStatusJoined, participant.Status)

		// Joined -> Ready
		err = participant.MarkReady()
		require.NoError(t, err)
		assert.Equal(t, value_objects.ParticipantStatusReady, participant.Status)

		// Ready -> Completed
		err = participant.MarkCompleted()
		require.NoError(t, err)
		assert.Equal(t, value_objects.ParticipantStatusCompleted, participant.Status)
		assert.NotNil(t, participant.CompletedAt)
	})

	t.Run("should mark participant as failed", func(t *testing.T) {
		partyID, _ := value_objects.NewPartyID("party1")
		participant, _ := entities.NewParticipant(partyID, 0, entities.DeviceInfo{
			DeviceType: entities.DeviceTypeIOS,
			DeviceID:   "device1",
			Platform:   "ios",
			AppVersion: "1.0.0",
		})

		participant.MarkFailed()
		assert.Equal(t, value_objects.ParticipantStatusFailed, participant.Status)
	})
}

func TestSessionID(t *testing.T) {
	t.Run("should create new session ID", func(t *testing.T) {
		id := value_objects.NewSessionID()
		assert.False(t, id.IsZero())
	})

	t.Run("should create session ID from string", func(t *testing.T) {
		original := value_objects.NewSessionID()
		parsed, err := value_objects.SessionIDFromString(original.String())
		require.NoError(t, err)
		assert.True(t, original.Equals(parsed))
	})

	t.Run("should fail to parse invalid session ID", func(t *testing.T) {
		_, err := value_objects.SessionIDFromString("invalid-uuid")
		assert.Error(t, err)
	})
}

func TestPartyID(t *testing.T) {
	t.Run("should create party ID", func(t *testing.T) {
		id, err := value_objects.NewPartyID("party1")
		require.NoError(t, err)
		assert.Equal(t, "party1", id.String())
		assert.False(t, id.IsZero())
	})

	t.Run("should fail with empty party ID", func(t *testing.T) {
		_, err := value_objects.NewPartyID("")
		assert.Error(t, err)
	})

	t.Run("should compare party IDs correctly", func(t *testing.T) {
		id1, _ := value_objects.NewPartyID("party1")
		id2, _ := value_objects.NewPartyID("party1")
		id3, _ := value_objects.NewPartyID("party2")

		assert.True(t, id1.Equals(id2))
		assert.False(t, id1.Equals(id3))
	})
}
