package main_test

import (
	"bytes"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha512"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/schanzen/taler-go/pkg/merchant"
	talerutil "github.com/schanzen/taler-go/pkg/util"
	"gopkg.in/ini.v1"
	"gorm.io/driver/sqlite"
	"taler.net/taler-mailbox/internal/gana"
	"taler.net/taler-mailbox/internal/util"
	"taler.net/taler-mailbox/pkg/rest"
)

var a mailbox.Mailbox

var testAliceSigningKeyPriv ed25519.PrivateKey
var testAliceSigningKey ed25519.PublicKey
var testAliceHashedSigningKeyString string

const merchantConfigResponse = `{
  "currency": "KUDOS",
  "currencies": {
    "KUDOS": {
      "name": "Kudos (Taler Demonstrator)",
      "currency": "KUDOS",
      "num_fractional_input_digits": 2,
      "num_fractional_normal_digits": 2,
      "num_fractional_trailing_zero_digits": 2,
      "alt_unit_names": {
        "0": "ク"
      }
    }
  },
  "exchanges": [
    {
      "master_pub": "F80MFRG8HVH6R9CQ47KRFQSJP3T6DBJ4K1D9B703RJY3Z39TBMJ0",
      "currency": "KUDOS",
      "base_url": "https://exchange.demo.taler.net/"
    }
  ],
  "implementation": "urn:net:taler:specs:taler-merchant:c-reference",
  "name": "taler-merchant",
  "version": "18:0:15"
}`

func executeRequest(req *http.Request) *httptest.ResponseRecorder {
	rr := httptest.NewRecorder()
	a.Router.ServeHTTP(rr, req)
	return rr
}

func checkResponseCode(t *testing.T, expected, actual int) bool {
	if expected != actual {
		t.Errorf("Expected response code %d, Got %d\n", expected, actual)
	}
	return expected == actual
}

var merchServerRespondsPaid = false

func shouldReturnPaid() bool {
	return merchServerRespondsPaid
}

func TestMain(m *testing.M) {
	cfg, err := ini.Load("test-mailbox.conf")
	if err != nil {
		fmt.Printf("Failed to read config: %v", err)
		os.Exit(1)
	}
	db := sqlite.Open("file::memory:?cache=shared")
	merchServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var orderResp merchant.PostOrderRequest
		if r.URL.Path == "/config" {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(merchantConfigResponse))
			return
		}
		if !strings.HasPrefix(r.URL.Path, "/private/orders") {
			fmt.Printf("Expected to request '/private/orders', got: %s\n", r.URL.Path)
			return
		}
		if r.Method == http.MethodPost {
			err := json.NewDecoder(r.Body).Decode(&orderResp)
			if err != nil {
				fmt.Printf("Error %s\n", err)
			}
			jsonResp := fmt.Sprintf("{\"order_id\":\"%s\"}", "uniqueOrderId")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(jsonResp))
		} else {
			fmt.Printf("Responding always paid: %v\n", merchServerRespondsPaid)
			if shouldReturnPaid() {
				jsonResp := "{\"order_status\":\"paid\"}"
				w.WriteHeader(http.StatusOK)
				w.Write([]byte(jsonResp))
			} else {
				jsonResp := "{\"order_status\":\"unpaid\", \"taler_pay_uri\": \"somepaytouri\"}"
				w.WriteHeader(http.StatusOK)
				w.Write([]byte(jsonResp))
			}
		}
	}))
	defer merchServer.Close()
	merch := merchant.NewMerchant(merchServer.URL, "supersecret")
	a.Initialize(mailbox.MailboxConfig{
		Version:  "testing",
		DB:       db,
		Merchant: merch,
		Ini:      cfg})
	testAliceSigningKey, testAliceSigningKeyPriv, _ = ed25519.GenerateKey(nil)
	h := sha512.New()
	h.Write(testAliceSigningKey)
	testAliceHashedSigningKeyString = util.Base32CrockfordEncode(h.Sum(nil))

	a.Merchant = merchant.NewMerchant(merchServer.URL, "")

	code := m.Run()
	// Purge DB
	a.DB.Where("1 = 1").Delete(&mailbox.InboxEntry{})
	os.Exit(code)
}

func TestEmptyMailbox(t *testing.T) {
	a.DB.Where("1 = 1").Delete(&mailbox.InboxEntry{})
	req, _ := http.NewRequest("GET", "/"+testAliceHashedSigningKeyString, nil)
	response := executeRequest(req)

	checkResponseCode(t, http.StatusNoContent, response.Code)

	body := response.Body.String()
	if body != "" {
		t.Errorf("Expected empty response, Got %s", body)
	}
}

func TestSendMessage(t *testing.T) {
	testMessage := make([]byte, 256)
	a.DB.Where("1 = 1").Delete(&mailbox.InboxEntry{})
	req, _ := http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage))
	response := executeRequest(req)

	checkResponseCode(t, http.StatusNoContent, response.Code)

	body := response.Body.String()
	if body != "" {
		t.Errorf("Expected empty response, Got %s", body)
	}
}

func setMailboxPaid(isPaid bool) {
	var messageFee talerutil.Amount
	if isPaid {
		messageFee = talerutil.NewAmount("KUDOS", 1, 0)
	} else {
		messageFee = talerutil.NewAmount("KUDOS", 0, 0)
	}
	a.MessageFee = &messageFee
	a.FreeMessageQuota = 1
}

func TestSendMessagePaid(t *testing.T) {

	// Make paid
	setMailboxPaid(true)

	// Cleanup
	a.DB.Where("1 = 1").Delete(&mailbox.InboxEntry{})

	testMessage := make([]byte, 256)
	rand.Read(testMessage)
	req, _ := http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage))
	response := executeRequest(req)

	checkResponseCode(t, http.StatusNoContent, response.Code)

	body := response.Body.String()
	if body != "" {
		t.Errorf("Expected empty response, Got %s", body)
	}
	testMessage2 := make([]byte, 256)
	rand.Read(testMessage2)
	req, _ = http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage2))
	response = executeRequest(req)

	checkResponseCode(t, http.StatusPaymentRequired, response.Code)

	body = response.Body.String()
	if body != "" {
		t.Errorf("Expected empty response, Got %s", body)
	}
	setMailboxPaid(false)
}

func TestGetKeysEmpty(t *testing.T) {
	req, _ := http.NewRequest("GET", "/info/"+testAliceHashedSigningKeyString, nil)
	response := executeRequest(req)
	checkResponseCode(t, http.StatusNotFound, response.Code)
}

func TestMailboxRegistration(t *testing.T) {
	var msg mailbox.MailboxRegistrationRequest
	// Dummy pubkey
	encKey := make([]byte, 32)
	aliceSigningKey := util.Base32CrockfordEncode(testAliceSigningKey)
	msg.MailboxMetadata.EncryptionKey = util.Base32CrockfordEncode(encKey)
	msg.MailboxMetadata.EncryptionKeyType = "X25519"
	msg.MailboxMetadata.Expiration = mailbox.Timestamp{Seconds: uint64(time.Now().Add(time.Hour*24*365).UnixMilli() / 1000)}
	msg.MailboxMetadata.SigningKey = aliceSigningKey
	msg.MailboxMetadata.SigningKeyType = "EdDSA"
	expNbo := make([]byte, 8)
	binary.BigEndian.PutUint64(expNbo, msg.MailboxMetadata.Expiration.Seconds)
	h := sha512.New()
	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType))
	h.Write(encKey)
	h.Write(expNbo)
	var signedMsg [64 + 4 + 4]byte
	size := signedMsg[0:4]
	binary.BigEndian.PutUint32(size, 64+4+4)
	purp := signedMsg[4:8]
	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
	copy(signedMsg[8:], h.Sum(nil))
	sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
	if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
		t.Errorf("Signature invalid!")
	}
	msg.Signature = util.Base32CrockfordEncode(sig)
	jsonMsg, _ := json.Marshal(msg)
	req, _ := http.NewRequest("POST", "/register", bytes.NewReader(jsonMsg))
	response := executeRequest(req)
	checkResponseCode(t, http.StatusNoContent, response.Code)
	req, _ = http.NewRequest("GET", "/info/"+testAliceHashedSigningKeyString, nil)
	response = executeRequest(req)
	checkResponseCode(t, http.StatusOK, response.Code)
	body := response.Body.String()
	if body == "" {
		t.Errorf("Expected response, Got %s", body)
		return
	}
	var respMsg mailbox.MailboxMetadata
	err := json.NewDecoder(response.Body).Decode(&respMsg)
	if err != nil {
		t.Errorf("Error %s\n", err)
	}
	if respMsg.SigningKey != msg.MailboxMetadata.SigningKey {
		fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
	}
	if respMsg.EncryptionKey != msg.MailboxMetadata.EncryptionKey {
		fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
	}
	a.DB.Where("1 = 1").Delete(&mailbox.MailboxMetadata{})
	a.DB.Where("1 = 1").Delete(&mailbox.PendingMailboxRegistration{})
}

func TestMailboxRegistrationPaid(t *testing.T) {
	var msg mailbox.MailboxRegistrationRequest

	// Make paid
	registrationUpdateFee := talerutil.NewAmount("KUDOS", 1, 0)
	monthlyFee := talerutil.NewAmount("KUDOS", 2, 0)
	a.RegistrationUpdateFee = &registrationUpdateFee
	a.MonthlyFee = &monthlyFee

	// Dummy pubkey
	encKey := make([]byte, 32)
	aliceSigningKey := util.Base32CrockfordEncode(testAliceSigningKey)
	msg.MailboxMetadata.EncryptionKey = util.Base32CrockfordEncode(encKey)
	msg.MailboxMetadata.EncryptionKeyType = "X25519"
	msg.MailboxMetadata.Expiration = mailbox.Timestamp{Seconds: uint64(time.Now().Add(time.Hour * 24 * 365).UnixMicro())}
	msg.MailboxMetadata.SigningKey = aliceSigningKey
	msg.MailboxMetadata.SigningKeyType = "EdDSA"
	expNbo := make([]byte, 8)
	binary.BigEndian.PutUint64(expNbo, msg.MailboxMetadata.Expiration.Seconds)
	h := sha512.New()
	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType))
	h.Write(encKey)
	h.Write(expNbo)
	var signedMsg [64 + 4 + 4]byte
	size := signedMsg[0:4]
	binary.BigEndian.PutUint32(size, 64+4+4)
	purp := signedMsg[4:8]
	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
	copy(signedMsg[8:], h.Sum(nil))
	sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
	if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
		t.Errorf("Signature invalid!")
	}
	msg.Signature = util.Base32CrockfordEncode(sig)
	jsonMsg, _ := json.Marshal(msg)
	req, _ := http.NewRequest("POST", "/register", bytes.NewReader(jsonMsg))
	response := executeRequest(req)
	checkResponseCode(t, http.StatusPaymentRequired, response.Code)

	req, _ = http.NewRequest("GET", "/info/"+testAliceHashedSigningKeyString, nil)
	response = executeRequest(req)
	checkResponseCode(t, http.StatusNotFound, response.Code)

	merchServerRespondsPaid = true
	req, _ = http.NewRequest("GET", "/info/"+testAliceHashedSigningKeyString, nil)
	response = executeRequest(req)
	checkResponseCode(t, http.StatusOK, response.Code)
	merchServerRespondsPaid = false

	body := response.Body.String()
	if body == "" {
		t.Errorf("Expected response, Got %s", body)
		return
	}
	var respMsg mailbox.MailboxMetadata
	err := json.NewDecoder(response.Body).Decode(&respMsg)
	if err != nil {
		fmt.Printf("Error %s\n", err)
	}
	if respMsg.SigningKey != msg.MailboxMetadata.SigningKey {
		fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
	}
	if respMsg.EncryptionKey != msg.MailboxMetadata.EncryptionKey {
		fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
	}
}

func TestPostThenDeleteMessage(t *testing.T) {

	// make not paid
	numMessagesToPost := (a.MessageResponseLimit + 7)
	testMessages := make([]byte, 256*numMessagesToPost)
	_, _ = rand.Read(testMessages)
	a.DB.Where("1 = 1").Delete(&mailbox.InboxEntry{})

	for i := 0; i < int(numMessagesToPost); i++ {
		testMessage := testMessages[i*256 : (i+1)*256]
		req, _ := http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage))
		response := executeRequest(req)

		checkResponseCode(t, http.StatusNoContent, response.Code)

		body := response.Body.String()
		if body != "" {
			t.Errorf("Expected empty response, Got %s", body)
		}
	}

	req, _ := http.NewRequest("GET", "/"+testAliceHashedSigningKeyString, nil)
	response := executeRequest(req)

	checkResponseCode(t, http.StatusOK, response.Code)

	if response.Body.Len() != int(256*a.MessageResponseLimit) {
		t.Errorf("Expected response of 25600 bytes, Got %d", response.Body.Len())
	}

	etag := response.Result().Header.Get("ETag")

	if etag == "" {
		t.Errorf("ETag missing!\n")
	}

	// Now  delete 10 messages
	h := sha512.New()
	for i := 0; i < int(a.MessageResponseLimit); i++ {
		h.Write(testMessages[i*256 : (i+1)*256])
	}
	etagInt, _ := strconv.Atoi(etag)
	var signedMsg [4 * 4]byte
	binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
	binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
	binary.BigEndian.PutUint32(signedMsg[8:12], uint32(etagInt))
	binary.BigEndian.PutUint32(signedMsg[12:16], uint32(a.MessageResponseLimit))
	sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
	if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
		t.Errorf("Signature invalid!")
	}
	hAddress := util.Base32CrockfordEncode(testAliceSigningKey)
	req, _ = http.NewRequest("DELETE", "/"+hAddress+"?count="+strconv.Itoa(int(a.MessageResponseLimit)), nil)
	req.Header.Add("If-Match", etag)
	req.Header.Add("Taler-Mailbox-Delete-Signature", util.Base32CrockfordEncode(sig))
	response = executeRequest(req)

	checkResponseCode(t, http.StatusNoContent, response.Code)

	body := response.Body.String()
	if body != "" {
		t.Errorf("Expected empty response, Got %s", body)
	}

	req, _ = http.NewRequest("GET", "/"+testAliceHashedSigningKeyString, nil)
	response = executeRequest(req)

	checkResponseCode(t, http.StatusOK, response.Code)

	if response.Body.Len() != int(256*7) {
		t.Errorf("Expected response of 256*7 bytes, Got %d", response.Body.Len())
	}
}
