package server

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"lbryio/wallet-sync-server/auth"
	"lbryio/wallet-sync-server/server/paths"
	"lbryio/wallet-sync-server/store"
	"lbryio/wallet-sync-server/wallet"
)

func TestServerGetWallet(t *testing.T) {
	tt := []struct {
		name        string
		tokenString auth.AuthTokenString

		expectedStatusCode  int
		expectedErrorString string

		storeErrors TestStoreFunctionsErrors
	}{
		{
			name:               "success",
			tokenString:        auth.AuthTokenString("seekrit"),
			expectedStatusCode: http.StatusOK,
		},
		{
			name:                "validation error", // missing auth token
			tokenString:         auth.AuthTokenString(""),
			expectedStatusCode:  http.StatusBadRequest,
			expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Missing token parameter",

			// Just check one validation error (missing auth token) to make sure the
			// validate function is called. We'll check the rest of the validation
			// errors in the other test below.
		},
		{
			name:        "auth error",
			tokenString: auth.AuthTokenString("seekrit"),

			expectedStatusCode:  http.StatusUnauthorized,
			expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",

			storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
		},
		{
			name:        "db error getting wallet",
			tokenString: auth.AuthTokenString("seekrit"),

			expectedStatusCode:  http.StatusInternalServerError,
			expectedErrorString: http.StatusText(http.StatusInternalServerError),

			storeErrors: TestStoreFunctionsErrors{GetWallet: fmt.Errorf("Some random DB Error!")},
		},
	}
	for _, tc := range tt {
		t.Run(tc.name, func(t *testing.T) {

			testAuth := TestAuth{}
			testStore := TestStore{
				TestAuthToken: auth.AuthToken{
					Token: auth.AuthTokenString(tc.tokenString),
					Scope: auth.ScopeFull,
				},

				TestEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
				TestSequence:        wallet.Sequence(2),
				TestHmac:            wallet.WalletHmac("my-hmac"),

				Errors: tc.storeErrors,
			}

			testEnv := TestEnv{}
			s := Init(&testAuth, &testStore, &testEnv, &TestMail{}, TestPort)

			req := httptest.NewRequest(http.MethodGet, paths.PathWallet, nil)
			q := req.URL.Query()
			q.Add("token", string(testStore.TestAuthToken.Token))
			req.URL.RawQuery = q.Encode()
			w := httptest.NewRecorder()

			// test handleWallet while we're at it, which is a dispatch for get and post
			// wallet
			s.handleWallet(w, req)

			// Make sure we tried to get an auth based on the `token` param (whether or
			// not it was a valid `token`)
			// NOTE: For tests that set testStore.TestAuthToken.Token=="", this will
			// pass even if GetToken isn't called. But we don't care, we expect the
			// request to fail for other reasons at that point.
			if want, got := testStore.TestAuthToken.Token, testStore.Called.GetToken; want != got {
				t.Errorf("testStore.Called.GetToken called with: expected %s, got %s", want, got)
			}

			body, _ := ioutil.ReadAll(w.Body)

			expectStatusCode(t, w, tc.expectedStatusCode)
			expectErrorString(t, body, tc.expectedErrorString)

			// In this case, a wallet body is expected iff there is no error string
			expectWalletBody := len(tc.expectedErrorString) == 0

			if !expectWalletBody {
				return // The rest of the test does not apply
			}

			var result WalletResponse
			err := json.Unmarshal(body, &result)

			if err != nil ||
				result.EncryptedWallet != testStore.TestEncryptedWallet ||
				result.Hmac != testStore.TestHmac ||
				result.Sequence != testStore.TestSequence {
				t.Errorf("Expected wallet response to have the test wallet values: result: %+v err: %+v", string(body), err)
			}

			if !testStore.Called.GetWallet {
				t.Errorf("Expected Store.GetWallet to be called")
			}
		})
	}
}

func TestServerPostWallet(t *testing.T) {
	tt := []struct {
		name string

		expectedStatusCode  int
		expectedErrorString string
		expectSetWalletCall bool
		expectWsMsg         bool

		// This is getting messy, but in the case of validation failures, we don't
		// even get around to trying to get an auth token, since the token string is
		// part of what's being validated. So, we want to be able to skip that
		// check in that case.
		skipAuthCheck bool

		// `new...` refers to what is being passed into the via POST request (and
		//   what we expect to get passed into SetWallet for the *non-error* cases
		//   below)
		newEncryptedWallet wallet.EncryptedWallet
		newSequence        wallet.Sequence
		newHmac            wallet.WalletHmac

		storeErrors TestStoreFunctionsErrors
	}{
		{
			name:                "success",
			expectedStatusCode:  http.StatusOK,
			expectSetWalletCall: true,
			expectWsMsg:         true,

			// Simulates a situation where the existing sequence is 1, the new
			// sequence is 2.

			newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
			newSequence:        wallet.Sequence(2),
			newHmac:            wallet.WalletHmac("my-hmac"),
		}, {
			name:                "conflict",
			expectedStatusCode:  http.StatusConflict,
			expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number",
			expectSetWalletCall: true,

			// Simulates a situation where the existing sequence is *not* 1, the new
			// proposed sequence is 2, and it thus fails with a conflict.

			newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-new"),
			newSequence:        wallet.Sequence(2),
			newHmac:            wallet.WalletHmac("my-hmac-new"),

			storeErrors: TestStoreFunctionsErrors{SetWallet: store.ErrWrongSequence},
		}, {
			name:                "validation error",
			expectedStatusCode:  http.StatusBadRequest,
			expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: Missing 'encryptedWallet'",
			skipAuthCheck:       true, // we can't get an auth token without the data we just failed to validate

			// Just check one validation error (empty encrypted wallet) to make sure the
			// validate function is called. We'll check the rest of the validation
			// errors in the other test below.

			newEncryptedWallet: wallet.EncryptedWallet(""),
			newSequence:        wallet.Sequence(2),
			newHmac:            wallet.WalletHmac("my-hmac"),
		}, {
			name:                "auth error",
			expectedStatusCode:  http.StatusUnauthorized,
			expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",

			// Putting in valid data here so it's clear that this isn't what causes
			// the error
			newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
			newSequence:        wallet.Sequence(2),
			newHmac:            wallet.WalletHmac("my-hmac"),

			// What causes the error
			storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
		}, {
			name:                "db error setting wallet",
			expectedStatusCode:  http.StatusInternalServerError,
			expectedErrorString: http.StatusText(http.StatusInternalServerError),
			expectSetWalletCall: true,

			// Putting in valid data here so it's clear that this isn't what causes
			// the error
			newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
			newSequence:        wallet.Sequence(2),
			newHmac:            wallet.WalletHmac("my-hmac"),

			// What causes the error
			storeErrors: TestStoreFunctionsErrors{SetWallet: fmt.Errorf("Some random db problem")},
		},

		// TODO
		// Future test case when we get lastSynced back: Error if
		// lastSynced.device_id doesn't match authToken.device_id

	}
	for _, tc := range tt {
		t.Run(tc.name, func(t *testing.T) {
			testAuth := TestAuth{}
			testStore := TestStore{
				TestAuthToken: auth.AuthToken{
					Token:  auth.AuthTokenString("seekrit"),
					Scope:  auth.ScopeFull,
					UserId: auth.UserId(37),
				},

				Errors: tc.storeErrors,
			}

			s := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
			wsmm := wsMockManager{s: s, done: make(chan bool)}

			requestBody := []byte(
				fmt.Sprintf(`{
          "token": "%s",
          "encryptedWallet": "%s",
          "sequence": %d,
          "hmac": "%s"
        }`, testStore.TestAuthToken.Token, tc.newEncryptedWallet, tc.newSequence, tc.newHmac),
			)

			req := httptest.NewRequest(http.MethodPost, paths.PathWallet, bytes.NewBuffer(requestBody))
			w := httptest.NewRecorder()

			// test handleWallet while we're at it, which is a dispatch for get and post
			// wallet
			go wsmm.getOneMessage(100 * time.Millisecond)
			s.handleWallet(w, req)
			<-wsmm.done
			if tc.expectWsMsg && wsmm.walletUpdateUserId != testStore.TestAuthToken.UserId {
				t.Error("Expected websocket message to update wallet")
			}
			if !tc.expectWsMsg && wsmm.walletUpdateUserId == testStore.TestAuthToken.UserId {
				t.Error("Expected no websocket message to update wallet")
			}

			// Make sure we tried to get an auth based on the `token` param (whether or
			// not it was a valid `token`)
			if want, got := testStore.TestAuthToken.Token, testStore.Called.GetToken; !tc.skipAuthCheck && want != got {
				t.Errorf("testStore.Called.GetToken called with: expected %s, got %s", want, got)
			}

			body, _ := ioutil.ReadAll(w.Body)

			expectStatusCode(t, w, tc.expectedStatusCode)
			expectErrorString(t, body, tc.expectedErrorString)

			if tc.expectedErrorString == "" && string(body) != "{}" {
				t.Errorf("Expected post wallet response to be \"{}\": result: %+v", string(body))
			}

			if want, got := (SetWalletCall{tc.newEncryptedWallet, tc.newSequence, tc.newHmac}), testStore.Called.SetWallet; tc.expectSetWalletCall && want != got {
				t.Errorf("Store.SetWallet called with: expected %+v, got %+v", want, got)
			}
		})
	}
}

func TestServerValidateWalletRequest(t *testing.T) {
	walletRequest := WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 2}
	if walletRequest.validate() != nil {
		t.Errorf("Expected valid WalletRequest to successfully validate")
	}

	tt := []struct {
		walletRequest       WalletRequest
		expectedErrorSubstr string
		failureDescription  string
	}{
		{
			WalletRequest{EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 2},
			"token",
			"Expected WalletRequest with missing token to not successfully validate",
		}, {
			WalletRequest{Token: "seekrit", Hmac: "my-hmac", Sequence: 2},
			"encryptedWallet",
			"Expected WalletRequest with missing encrypted wallet to not successfully validate",
		}, {
			WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Sequence: 2},
			"hmac",
			"Expected WalletRequest with missing hmac to not successfully validate",
		}, {
			WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 0},
			"sequence",
			"Expected WalletRequest with sequence < 1 to not successfully validate",
		},
	}
	for _, tc := range tt {
		err := tc.walletRequest.validate()
		if err == nil || !strings.Contains(err.Error(), tc.expectedErrorSubstr) {
			t.Errorf(tc.failureDescription)
		}
	}
}