//go:build integration

package integration_test

import (
	"context"
	"database/sql"
	"os"
	"testing"
	"time"

	_ "github.com/lib/pq"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

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

type SessionRepositoryTestSuite struct {
	suite.Suite
	db          *sql.DB
	sessionRepo *postgres.SessionPostgresRepo
	messageRepo *postgres.MessagePostgresRepo
	ctx         context.Context
}

func TestSessionRepositorySuite(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}
	suite.Run(t, new(SessionRepositoryTestSuite))
}

func (s *SessionRepositoryTestSuite) SetupSuite() {
	// Get database connection string from environment
	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		dsn = "postgres://mpc_user:mpc_password@localhost:5433/mpc_system_test?sslmode=disable"
	}

	var err error
	s.db, err = sql.Open("postgres", dsn)
	require.NoError(s.T(), err)

	err = s.db.Ping()
	require.NoError(s.T(), err, "Failed to connect to test database")

	s.sessionRepo = postgres.NewSessionPostgresRepo(s.db)
	s.messageRepo = postgres.NewMessagePostgresRepo(s.db)
	s.ctx = context.Background()

	// Run migrations or setup test schema
	s.setupTestSchema()
}

func (s *SessionRepositoryTestSuite) TearDownSuite() {
	if s.db != nil {
		s.db.Close()
	}
}

func (s *SessionRepositoryTestSuite) SetupTest() {
	// Clean up test data before each test
	s.cleanupTestData()
}

func (s *SessionRepositoryTestSuite) setupTestSchema() {
	// Ensure tables exist (in real scenario, you'd run migrations)
	// This is a simplified version for testing
}

func (s *SessionRepositoryTestSuite) cleanupTestData() {
	// Clean up test data - order matters due to foreign keys
	_, err := s.db.ExecContext(s.ctx, "DELETE FROM mpc_messages WHERE session_id IN (SELECT id FROM mpc_sessions WHERE created_by LIKE 'test_%')")
	if err != nil {
		s.T().Logf("Warning: failed to clean messages: %v", err)
	}
	_, err = s.db.ExecContext(s.ctx, "DELETE FROM participants WHERE session_id IN (SELECT id FROM mpc_sessions WHERE created_by LIKE 'test_%')")
	if err != nil {
		s.T().Logf("Warning: failed to clean participants: %v", err)
	}
	_, err = s.db.ExecContext(s.ctx, "DELETE FROM mpc_sessions WHERE created_by LIKE 'test_%'")
	if err != nil {
		s.T().Logf("Warning: failed to clean sessions: %v", err)
	}
}

func (s *SessionRepositoryTestSuite) TestCreateSession() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_1", 30*time.Minute, nil)
	require.NoError(s.T(), err)

	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Verify session was created
	retrieved, err := s.sessionRepo.FindByID(s.ctx, session.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), session.ID, retrieved.ID)
	assert.Equal(s.T(), session.SessionType, retrieved.SessionType)
	assert.Equal(s.T(), session.Threshold.T(), retrieved.Threshold.T())
	assert.Equal(s.T(), session.Threshold.N(), retrieved.Threshold.N())
	assert.Equal(s.T(), session.Status, retrieved.Status)
}

func (s *SessionRepositoryTestSuite) TestUpdateSession() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_2", 30*time.Minute, nil)
	require.NoError(s.T(), err)

	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Add required participants before starting
	for i := 0; i < 3; i++ {
		deviceInfo := entities.DeviceInfo{
			DeviceType: "iOS",
			DeviceID:   "device" + string(rune('0'+i)),
		}
		partyID, _ := value_objects.NewPartyID("test_party_update_" + string(rune('a'+i)))
		participant, _ := entities.NewParticipant(partyID, i, deviceInfo)
		participant.Join() // Mark participant as joined
		err = session.AddParticipant(participant)
		require.NoError(s.T(), err)
	}

	// Now start the session
	err = session.Start()
	require.NoError(s.T(), err)

	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Verify update
	retrieved, err := s.sessionRepo.FindByID(s.ctx, session.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), value_objects.SessionStatusInProgress, retrieved.Status)
}

func (s *SessionRepositoryTestSuite) TestGetByID_NotFound() {
	nonExistentID := value_objects.NewSessionID()

	_, err := s.sessionRepo.FindByID(s.ctx, nonExistentID)
	assert.Error(s.T(), err)
}

func (s *SessionRepositoryTestSuite) TestListActiveSessions() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	// Create session with created status
	activeSession, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_3", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, activeSession)
	require.NoError(s.T(), err)

	// Create session with in_progress status
	inProgressSession, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_4", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	// Add all required participants
	for i := 0; i < 3; i++ {
		deviceInfo := entities.DeviceInfo{DeviceType: "test", DeviceID: "device" + string(rune('a'+i))}
		partyID, _ := value_objects.NewPartyID("party_in_progress_" + string(rune('a'+i)))
		participant, _ := entities.NewParticipant(partyID, i, deviceInfo)
		participant.Join() // Mark as joined
		inProgressSession.AddParticipant(participant)
	}
	err = inProgressSession.Start()
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, inProgressSession)
	require.NoError(s.T(), err)

	// Create session with completed status
	completedSession, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_5", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	// Add all required participants
	for i := 0; i < 3; i++ {
		deviceInfo := entities.DeviceInfo{DeviceType: "test", DeviceID: "device" + string(rune('a'+i))}
		partyID, _ := value_objects.NewPartyID("party_completed_" + string(rune('a'+i)))
		participant, _ := entities.NewParticipant(partyID, i, deviceInfo)
		participant.Join() // Mark as joined
		completedSession.AddParticipant(participant)
	}
	err = completedSession.Start()
	require.NoError(s.T(), err)
	err = completedSession.Complete([]byte("test-public-key"))
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, completedSession)
	require.NoError(s.T(), err)

	// List sessions by status (use FindByStatus instead of FindActive)
	createdSessions, err := s.sessionRepo.FindByStatus(s.ctx, value_objects.SessionStatusCreated)
	require.NoError(s.T(), err)
	inProgressSessions, err := s.sessionRepo.FindByStatus(s.ctx, value_objects.SessionStatusInProgress)
	require.NoError(s.T(), err)
	activeSessions := append(createdSessions, inProgressSessions...)

	// Should include created and in_progress sessions
	activeCount := 0
	for _, session := range activeSessions {
		if session.Status == value_objects.SessionStatusCreated ||
			session.Status == value_objects.SessionStatusInProgress {
			activeCount++
		}
	}
	assert.GreaterOrEqual(s.T(), activeCount, 2)
}

func (s *SessionRepositoryTestSuite) TestGetExpiredSessions() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	// Create an expired session
	expiredSession, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_6", -1*time.Hour, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, expiredSession)
	require.NoError(s.T(), err)

	// Get expired sessions
	expiredSessions, err := s.sessionRepo.FindExpired(s.ctx)
	require.NoError(s.T(), err)

	// Should find at least one expired session
	found := false
	for _, session := range expiredSessions {
		if session.ID.Equals(expiredSession.ID) {
			found = true
			break
		}
	}
	assert.True(s.T(), found, "Should find the expired session")
}

func (s *SessionRepositoryTestSuite) TestAddParticipant() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_7", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Add participant
	deviceInfo := entities.DeviceInfo{
		DeviceType: "iOS",
		DeviceID:   "device123",
	}
	partyID, err := value_objects.NewPartyID("test_party_1")
	require.NoError(s.T(), err)
	participant, err := entities.NewParticipant(
		partyID,
		0,
		deviceInfo,
	)
	require.NoError(s.T(), err)

	err = session.AddParticipant(participant)
	require.NoError(s.T(), err)

	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Retrieve session and check participants
	retrieved, err := s.sessionRepo.FindByID(s.ctx, session.ID)
	require.NoError(s.T(), err)
	assert.Len(s.T(), retrieved.Participants, 1)
	assert.Equal(s.T(), "test_party_1", retrieved.Participants[0].PartyID.String())
}

func (s *SessionRepositoryTestSuite) TestUpdateParticipant() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_8", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	deviceInfo := entities.DeviceInfo{
		DeviceType: "iOS",
		DeviceID:   "device123",
	}
	partyID, err := value_objects.NewPartyID("test_party_2")
	require.NoError(s.T(), err)
	participant, err := entities.NewParticipant(
		partyID,
		0,
		deviceInfo,
	)
	require.NoError(s.T(), err)

	err = session.AddParticipant(participant)
	require.NoError(s.T(), err)

	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Update participant status
	participant.Join() // Must transition to Joined first
	err = participant.MarkReady()
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Verify update
	retrieved, err := s.sessionRepo.FindByID(s.ctx, session.ID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), value_objects.ParticipantStatusReady, retrieved.Participants[0].Status)
}

func (s *SessionRepositoryTestSuite) TestDeleteSession() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_9", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Delete session
	err = s.sessionRepo.Delete(s.ctx, session.ID)
	require.NoError(s.T(), err)

	// Verify deletion
	_, err = s.sessionRepo.FindByID(s.ctx, session.ID)
	assert.Error(s.T(), err)
}

// Message Repository Tests

func (s *SessionRepositoryTestSuite) TestCreateMessage() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_10", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	senderID, _ := value_objects.NewPartyID("sender")
	receiverID, _ := value_objects.NewPartyID("receiver")
	message := entities.NewSessionMessage(
		session.ID,
		senderID,
		[]value_objects.PartyID{receiverID},
		1,
		"keygen_round1",
		[]byte("encrypted payload"),
	)

	err = s.messageRepo.SaveMessage(s.ctx, message)
	require.NoError(s.T(), err)

	// Message verification would require implementing FindByID method
	// For now, just verify save succeeded
}

func (s *SessionRepositoryTestSuite) TestGetPendingMessages() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_11", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	// Create pending message
	senderID, _ := value_objects.NewPartyID("sender")
	receiverID, _ := value_objects.NewPartyID("receiver")
	message := entities.NewSessionMessage(
		session.ID,
		senderID,
		[]value_objects.PartyID{receiverID},
		1,
		"keygen_round1",
		[]byte("payload"),
	)
	err = s.messageRepo.SaveMessage(s.ctx, message)
	require.NoError(s.T(), err)

	// Pending messages test would require implementing FindPendingForParty
	// Skipping for now as the save succeeded
}

func (s *SessionRepositoryTestSuite) TestMarkMessageDelivered() {
	threshold, err := value_objects.NewThreshold(2, 3)
	require.NoError(s.T(), err)

	session, err := entities.NewMPCSession(entities.SessionTypeKeygen, threshold, "test_user_12", 30*time.Minute, nil)
	require.NoError(s.T(), err)
	err = s.sessionRepo.Save(s.ctx, session)
	require.NoError(s.T(), err)

	senderID, _ := value_objects.NewPartyID("sender")
	receiverID, _ := value_objects.NewPartyID("receiver")
	message := entities.NewSessionMessage(
		session.ID,
		senderID,
		[]value_objects.PartyID{receiverID},
		1,
		"keygen_round1",
		[]byte("payload"),
	)
	err = s.messageRepo.SaveMessage(s.ctx, message)
	require.NoError(s.T(), err)

	// Mark as delivered (message.ID is already uuid.UUID)
	err = s.messageRepo.MarkDelivered(s.ctx, message.ID)
	require.NoError(s.T(), err)

	// Verify would require FindByID implementation
	// For now, just verify mark delivered succeeded
}
