// Copyright (c) 2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package database

import (
	"context"
	"crypto/ed25519"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"strconv"
	"sync"
	"testing"
	"time"
)

const (
	testDb               = "test.db"
	backupDb             = "test.db-backup"
	feeXPub              = "feexpub"
	maxVoteChangeRecords = 3
)

var (
	db *VspDatabase
)

// TestDatabase runs all database tests.
func TestDatabase(t *testing.T) {
	// Ensure we are starting with a clean environment.
	os.Remove(testDb)
	os.Remove(backupDb)

	// All sub-tests to run.
	tests := map[string]func(*testing.T){
		"testCreateNew":         testCreateNew,
		"testInsertNewTicket":   testInsertNewTicket,
		"testGetTicketByHash":   testGetTicketByHash,
		"testUpdateTicket":      testUpdateTicket,
		"testTicketFeeExpired":  testTicketFeeExpired,
		"testFilterTickets":     testFilterTickets,
		"testCountTickets":      testCountTickets,
		"testAddressIndex":      testAddressIndex,
		"testDeleteTicket":      testDeleteTicket,
		"testVoteChangeRecords": testVoteChangeRecords,
		"testHTTPBackup":        testHTTPBackup,
	}

	for testName, test := range tests {
		// Create a new blank database for each sub-test.
		var err error
		var wg sync.WaitGroup
		ctx, cancel := context.WithCancel(context.TODO())
		err = CreateNew(testDb, feeXPub)
		if err != nil {
			t.Fatalf("error creating test database: %v", err)
		}
		db, err = Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords)
		if err != nil {
			t.Fatalf("error opening test database: %v", err)
		}

		// Run the sub-test.
		t.Run(testName, test)

		// Request database shutdown and wait for it to complete.
		cancel()
		wg.Wait()

		os.Remove(testDb)
		os.Remove(backupDb)
	}
}

func testCreateNew(t *testing.T) {
	// A newly created DB should contain a signing keypair.
	priv, pub, err := db.KeyPair()
	if err != nil {
		t.Fatalf("error getting keypair: %v", err)
	}

	// Ensure keypair can be used for signing/verifying messages.
	msg := []byte("msg")
	sig := ed25519.Sign(priv, msg)
	if !ed25519.Verify(pub, msg, sig) {
		t.Fatalf("keypair from database could not be used to sign/verify a message")
	}

	// A newly created DB should have a cookie secret.
	secret, err := db.GetCookieSecret()
	if err != nil {
		t.Fatalf("error getting cookie secret: %v", err)
	}

	if len(secret) != 32 {
		t.Fatalf("expected a 32 byte cookie secret, got %d bytes", len(secret))
	}

	// A newly created DB should store the fee xpub it was initialized with.
	retrievedXPub, err := db.GetFeeXPub()
	if err != nil {
		t.Fatalf("error getting fee xpub: %v", err)
	}

	if retrievedXPub != feeXPub {
		t.Fatalf("expected fee xpub %v, got %v", feeXPub, retrievedXPub)
	}
}

func testHTTPBackup(t *testing.T) {
	// Capture the HTTP response written by the backup func.
	rr := httptest.NewRecorder()
	err := db.BackupDB(rr)
	if err != nil {
		t.Fatal(err)
	}

	// Check the HTTP status is OK.
	if status := rr.Code; status != http.StatusOK {
		t.Fatalf("wrong HTTP status code: expected %v, got %v",
			http.StatusOK, status)
	}

	// Check HTTP headers.
	header := "Content-Type"
	expected := "application/octet-stream"
	if actual := rr.Header().Get(header); actual != expected {
		t.Errorf("wrong %s header: expected %s, got %s",
			header, expected, actual)
	}

	header = "Content-Disposition"
	expected = `attachment; filename="vspd.db"`
	if actual := rr.Header().Get(header); actual != expected {
		t.Errorf("wrong %s header: expected %s, got %s",
			header, expected, actual)
	}

	header = "Content-Length"
	cLength, err := strconv.Atoi(rr.Header().Get(header))
	if err != nil {
		t.Fatalf("could not convert %s to integer: %v", header, err)
	}

	if cLength <= 0 {
		t.Fatalf("expected a %s greater than zero, got %d", header, cLength)
	}

	// Check reported length matches actual.
	body, err := ioutil.ReadAll(rr.Result().Body)
	if err != nil {
		t.Fatalf("could not read http response body: %v", err)
	}

	if len(body) != cLength {
		t.Fatalf("expected reported content-length to match actual body length. %v != %v",
			cLength, len(body))
	}
}
