//go:build e2e

package e2e_test

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"os"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/rwadurian/mpc-system/pkg/crypto"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
)

type AccountFlowTestSuite struct {
	suite.Suite
	baseURL string
	client  *http.Client
}

func TestAccountFlowSuite(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping e2e test in short mode")
	}
	suite.Run(t, new(AccountFlowTestSuite))
}

func (s *AccountFlowTestSuite) SetupSuite() {
	s.baseURL = os.Getenv("ACCOUNT_SERVICE_URL")
	if s.baseURL == "" {
		s.baseURL = "http://localhost:8083"
	}

	s.client = &http.Client{
		Timeout: 30 * time.Second,
	}

	s.waitForService()
}

func (s *AccountFlowTestSuite) waitForService() {
	maxRetries := 30
	for i := 0; i < maxRetries; i++ {
		resp, err := s.client.Get(s.baseURL + "/health")
		if err == nil && resp.StatusCode == http.StatusOK {
			resp.Body.Close()
			return
		}
		if resp != nil {
			resp.Body.Close()
		}
		time.Sleep(time.Second)
	}
	s.T().Fatal("Account service not ready after waiting")
}

type AccountCreateRequest struct {
	Username        string       `json:"username"`
	Email           string       `json:"email"`
	Phone           *string      `json:"phone"`
	PublicKey       string       `json:"publicKey"`
	KeygenSessionID string       `json:"keygenSessionId"`
	ThresholdN      int          `json:"thresholdN"`
	ThresholdT      int          `json:"thresholdT"`
	Shares          []ShareInput `json:"shares"`
}

type ShareInput struct {
	ShareType  string  `json:"shareType"`
	PartyID    string  `json:"partyId"`
	PartyIndex int     `json:"partyIndex"`
	DeviceType *string `json:"deviceType"`
	DeviceID   *string `json:"deviceId"`
}

type AccountResponse struct {
	Account struct {
		ID              string  `json:"id"`
		Username        string  `json:"username"`
		Email           string  `json:"email"`
		Phone           *string `json:"phone"`
		ThresholdN      int     `json:"thresholdN"`
		ThresholdT      int     `json:"thresholdT"`
		Status          string  `json:"status"`
		KeygenSessionID string  `json:"keygenSessionId"`
	} `json:"account"`
	Shares []struct {
		ID         string  `json:"id"`
		ShareType  string  `json:"shareType"`
		PartyID    string  `json:"partyId"`
		PartyIndex int     `json:"partyIndex"`
		DeviceType *string `json:"deviceType"`
		DeviceID   *string `json:"deviceId"`
		IsActive   bool    `json:"isActive"`
	} `json:"shares"`
}

type ChallengeResponse struct {
	ChallengeID string `json:"challengeId"`
	Challenge   string `json:"challenge"`
	ExpiresAt   string `json:"expiresAt"`
}

type LoginResponse struct {
	Account struct {
		ID       string `json:"id"`
		Username string `json:"username"`
	} `json:"account"`
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken"`
}

func (s *AccountFlowTestSuite) TestCompleteAccountFlow() {
	// Generate a test keypair
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(s.T(), err)
	publicKeyBytes := crypto.MarshalPublicKey(&privateKey.PublicKey)

	// Step 1: Create account
	uniqueID := uuid.New().String()[:8]
	phone := "+1234567890"
	deviceType := "iOS"
	deviceID := "test_device_001"

	createReq := AccountCreateRequest{
		Username:        "e2e_test_user_" + uniqueID,
		Email:           "e2e_test_" + uniqueID + "@example.com",
		Phone:           &phone,
		PublicKey:       hex.EncodeToString(publicKeyBytes),
		KeygenSessionID: uuid.New().String(),
		ThresholdN:      3,
		ThresholdT:      2,
		Shares: []ShareInput{
			{
				ShareType:  "user_device",
				PartyID:    "party_user_" + uniqueID,
				PartyIndex: 0,
				DeviceType: &deviceType,
				DeviceID:   &deviceID,
			},
			{
				ShareType:  "server",
				PartyID:    "party_server_" + uniqueID,
				PartyIndex: 1,
			},
			{
				ShareType:  "recovery",
				PartyID:    "party_recovery_" + uniqueID,
				PartyIndex: 2,
			},
		},
	}

	accountResp := s.createAccount(createReq)
	require.NotEmpty(s.T(), accountResp.Account.ID)
	assert.Equal(s.T(), createReq.Username, accountResp.Account.Username)
	assert.Equal(s.T(), createReq.Email, accountResp.Account.Email)
	assert.Equal(s.T(), "active", accountResp.Account.Status)
	assert.Len(s.T(), accountResp.Shares, 3)

	accountID := accountResp.Account.ID

	// Step 2: Get account by ID
	retrievedAccount := s.getAccount(accountID)
	assert.Equal(s.T(), accountID, retrievedAccount.Account.ID)

	// Step 3: Get account shares
	shares := s.getAccountShares(accountID)
	assert.Len(s.T(), shares, 3)

	// Step 4: Generate login challenge
	challengeResp := s.generateChallenge(createReq.Username)
	require.NotEmpty(s.T(), challengeResp.ChallengeID)
	require.NotEmpty(s.T(), challengeResp.Challenge)

	// Step 5: Sign challenge and login
	challengeBytes, _ := hex.DecodeString(challengeResp.Challenge)
	signature, err := crypto.SignMessage(privateKey, challengeBytes)
	require.NoError(s.T(), err)

	loginResp := s.login(createReq.Username, challengeResp.Challenge, hex.EncodeToString(signature))
	require.NotEmpty(s.T(), loginResp.AccessToken)
	require.NotEmpty(s.T(), loginResp.RefreshToken)

	// Step 6: Refresh token
	newTokens := s.refreshToken(loginResp.RefreshToken)
	require.NotEmpty(s.T(), newTokens.AccessToken)

	// Step 7: Update account
	newPhone := "+9876543210"
	s.updateAccount(accountID, &newPhone)

	updatedAccount := s.getAccount(accountID)
	assert.Equal(s.T(), newPhone, *updatedAccount.Account.Phone)

	// Step 8: Deactivate a share
	if len(shares) > 0 {
		shareID := shares[0].ID
		s.deactivateShare(accountID, shareID)

		updatedShares := s.getAccountShares(accountID)
		for _, share := range updatedShares {
			if share.ID == shareID {
				assert.False(s.T(), share.IsActive)
			}
		}
	}
}

func (s *AccountFlowTestSuite) TestAccountRecoveryFlow() {
	// Generate keypairs
	oldPrivateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	oldPublicKeyBytes := crypto.MarshalPublicKey(&oldPrivateKey.PublicKey)

	newPrivateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	newPublicKeyBytes := crypto.MarshalPublicKey(&newPrivateKey.PublicKey)

	// Create account
	uniqueID := uuid.New().String()[:8]
	createReq := AccountCreateRequest{
		Username:        "e2e_recovery_user_" + uniqueID,
		Email:           "e2e_recovery_" + uniqueID + "@example.com",
		PublicKey:       hex.EncodeToString(oldPublicKeyBytes),
		KeygenSessionID: uuid.New().String(),
		ThresholdN:      3,
		ThresholdT:      2,
		Shares: []ShareInput{
			{ShareType: "user_device", PartyID: "party_user_" + uniqueID, PartyIndex: 0},
			{ShareType: "server", PartyID: "party_server_" + uniqueID, PartyIndex: 1},
			{ShareType: "recovery", PartyID: "party_recovery_" + uniqueID, PartyIndex: 2},
		},
	}

	accountResp := s.createAccount(createReq)
	accountID := accountResp.Account.ID

	// Step 1: Initiate recovery
	oldShareType := "user_device"
	recoveryResp := s.initiateRecovery(accountID, "device_lost", &oldShareType)
	require.NotEmpty(s.T(), recoveryResp.RecoverySessionID)

	recoverySessionID := recoveryResp.RecoverySessionID

	// Step 2: Check recovery status
	recoveryStatus := s.getRecoveryStatus(recoverySessionID)
	assert.Equal(s.T(), "requested", recoveryStatus.Status)

	// Step 3: Complete recovery with new keys
	newKeygenSessionID := uuid.New().String()
	s.completeRecovery(recoverySessionID, hex.EncodeToString(newPublicKeyBytes), newKeygenSessionID, []ShareInput{
		{ShareType: "user_device", PartyID: "new_party_user_" + uniqueID, PartyIndex: 0},
		{ShareType: "server", PartyID: "new_party_server_" + uniqueID, PartyIndex: 1},
		{ShareType: "recovery", PartyID: "new_party_recovery_" + uniqueID, PartyIndex: 2},
	})

	// Step 4: Verify account is active again
	updatedAccount := s.getAccount(accountID)
	assert.Equal(s.T(), "active", updatedAccount.Account.Status)
}

func (s *AccountFlowTestSuite) TestDuplicateUsername() {
	uniqueID := uuid.New().String()[:8]
	privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	publicKeyBytes := crypto.MarshalPublicKey(&privateKey.PublicKey)

	createReq := AccountCreateRequest{
		Username:        "e2e_duplicate_" + uniqueID,
		Email:           "e2e_dup1_" + uniqueID + "@example.com",
		PublicKey:       hex.EncodeToString(publicKeyBytes),
		KeygenSessionID: uuid.New().String(),
		ThresholdN:      2,
		ThresholdT:      2,
		Shares: []ShareInput{
			{ShareType: "user_device", PartyID: "party1", PartyIndex: 0},
			{ShareType: "server", PartyID: "party2", PartyIndex: 1},
		},
	}

	// First account should succeed
	s.createAccount(createReq)

	// Second account with same username should fail
	createReq.Email = "e2e_dup2_" + uniqueID + "@example.com"
	body, _ := json.Marshal(createReq)
	resp, err := s.client.Post(
		s.baseURL+"/api/v1/accounts",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode) // Duplicate error
}

func (s *AccountFlowTestSuite) TestInvalidLogin() {
	// Try to login with non-existent user
	challengeResp := s.generateChallenge("nonexistent_user_xyz")

	// Even if challenge is generated, login should fail
	resp, err := s.client.Post(
		s.baseURL+"/api/v1/auth/login",
		"application/json",
		bytes.NewReader([]byte(`{"username":"nonexistent_user_xyz","challenge":"abc","signature":"def"}`)),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)

	_ = challengeResp // suppress unused variable warning
}

// Helper methods

func (s *AccountFlowTestSuite) createAccount(req AccountCreateRequest) AccountResponse {
	body, _ := json.Marshal(req)
	resp, err := s.client.Post(
		s.baseURL+"/api/v1/accounts",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusCreated, resp.StatusCode)

	var result AccountResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) getAccount(accountID string) AccountResponse {
	resp, err := s.client.Get(s.baseURL + "/api/v1/accounts/" + accountID)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result AccountResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) getAccountShares(accountID string) []struct {
	ID         string  `json:"id"`
	ShareType  string  `json:"shareType"`
	PartyID    string  `json:"partyId"`
	PartyIndex int     `json:"partyIndex"`
	DeviceType *string `json:"deviceType"`
	DeviceID   *string `json:"deviceId"`
	IsActive   bool    `json:"isActive"`
} {
	resp, err := s.client.Get(s.baseURL + "/api/v1/accounts/" + accountID + "/shares")
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result struct {
		Shares []struct {
			ID         string  `json:"id"`
			ShareType  string  `json:"shareType"`
			PartyID    string  `json:"partyId"`
			PartyIndex int     `json:"partyIndex"`
			DeviceType *string `json:"deviceType"`
			DeviceID   *string `json:"deviceId"`
			IsActive   bool    `json:"isActive"`
		} `json:"shares"`
	}
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result.Shares
}

func (s *AccountFlowTestSuite) generateChallenge(username string) ChallengeResponse {
	req := map[string]string{"username": username}
	body, _ := json.Marshal(req)

	resp, err := s.client.Post(
		s.baseURL+"/api/v1/auth/challenge",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result ChallengeResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) login(username, challenge, signature string) LoginResponse {
	req := map[string]string{
		"username":  username,
		"challenge": challenge,
		"signature": signature,
	}
	body, _ := json.Marshal(req)

	resp, err := s.client.Post(
		s.baseURL+"/api/v1/auth/login",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result LoginResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) refreshToken(refreshToken string) struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken"`
} {
	req := map[string]string{"refreshToken": refreshToken}
	body, _ := json.Marshal(req)

	resp, err := s.client.Post(
		s.baseURL+"/api/v1/auth/refresh",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result struct {
		AccessToken  string `json:"accessToken"`
		RefreshToken string `json:"refreshToken"`
	}
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) updateAccount(accountID string, phone *string) {
	req := map[string]*string{"phone": phone}
	body, _ := json.Marshal(req)

	httpReq, _ := http.NewRequest(
		http.MethodPut,
		s.baseURL+"/api/v1/accounts/"+accountID,
		bytes.NewReader(body),
	)
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := s.client.Do(httpReq)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)
}

func (s *AccountFlowTestSuite) deactivateShare(accountID, shareID string) {
	httpReq, _ := http.NewRequest(
		http.MethodDelete,
		s.baseURL+"/api/v1/accounts/"+accountID+"/shares/"+shareID,
		nil,
	)

	resp, err := s.client.Do(httpReq)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)
}

func (s *AccountFlowTestSuite) initiateRecovery(accountID, recoveryType string, oldShareType *string) struct {
	RecoverySessionID string `json:"recoverySessionId"`
} {
	req := map[string]interface{}{
		"accountId":    accountID,
		"recoveryType": recoveryType,
	}
	if oldShareType != nil {
		req["oldShareType"] = *oldShareType
	}
	body, _ := json.Marshal(req)

	resp, err := s.client.Post(
		s.baseURL+"/api/v1/recovery",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusCreated, resp.StatusCode)

	var result struct {
		RecoverySession struct {
			ID string `json:"id"`
		} `json:"recoverySession"`
	}
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return struct {
		RecoverySessionID string `json:"recoverySessionId"`
	}{
		RecoverySessionID: result.RecoverySession.ID,
	}
}

func (s *AccountFlowTestSuite) getRecoveryStatus(recoverySessionID string) struct {
	Status string `json:"status"`
} {
	resp, err := s.client.Get(s.baseURL + "/api/v1/recovery/" + recoverySessionID)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)

	var result struct {
		Status string `json:"status"`
	}
	err = json.NewDecoder(resp.Body).Decode(&result)
	require.NoError(s.T(), err)

	return result
}

func (s *AccountFlowTestSuite) completeRecovery(recoverySessionID, newPublicKey, newKeygenSessionID string, newShares []ShareInput) {
	req := map[string]interface{}{
		"newPublicKey":       newPublicKey,
		"newKeygenSessionId": newKeygenSessionID,
		"newShares":          newShares,
	}
	body, _ := json.Marshal(req)

	resp, err := s.client.Post(
		s.baseURL+"/api/v1/recovery/"+recoverySessionID+"/complete",
		"application/json",
		bytes.NewReader(body),
	)
	require.NoError(s.T(), err)
	defer resp.Body.Close()

	require.Equal(s.T(), http.StatusOK, resp.StatusCode)
}
