// Copyright (c) 2020-2022 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 (
	"reflect"
	"testing"
	"time"

	bolt "go.etcd.io/bbolt"
)

func exampleTicket() Ticket {
	return Ticket{
		Hash:              randString(64, hexCharset),
		CommitmentAddress: randString(35, addrCharset),
		FeeAddressIndex:   12345,
		FeeAddress:        randString(35, addrCharset),
		FeeAmount:         10000000,
		FeeExpiration:     4,
		Confirmed:         false,
		VoteChoices:       map[string]string{"AgendaID": "yes"},
		TSpendPolicy:      map[string]string{randString(64, hexCharset): "no"},
		TreasuryPolicy:    map[string]string{randString(66, hexCharset): "abstain"},
		VotingWIF:         randString(53, addrCharset),
		FeeTxHex:          randString(504, hexCharset),
		FeeTxHash:         randString(64, hexCharset),
		FeeTxStatus:       FeeBroadcast,
	}
}

func testInsertNewTicket(t *testing.T) {
	// Insert a ticket into the database.
	ticket := exampleTicket()
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Insert another ticket.
	err = db.InsertNewTicket(exampleTicket())
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Inserting a ticket with the same hash should fail.
	ticket2 := exampleTicket()
	ticket2.Hash = ticket.Hash
	err = db.InsertNewTicket(ticket2)
	if err == nil {
		t.Fatal("expected an error inserting ticket with duplicate hash")
	}

	// Inserting a ticket with the same fee address should fail.
	ticket3 := exampleTicket()
	ticket3.FeeAddress = ticket.FeeAddress
	err = db.InsertNewTicket(ticket3)
	if err == nil {
		t.Fatal("expected an error inserting ticket with duplicate fee addr")
	}

	// Inserting a ticket with empty hash should fail.
	ticket.Hash = ""
	err = db.InsertNewTicket(ticket)
	if err == nil {
		t.Fatal("expected an error inserting ticket with no hash")
	}
}

func testDeleteTicket(t *testing.T) {
	// Insert a ticket into the database.
	ticket := exampleTicket()
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Delete ticket.
	err = db.DeleteTicket(ticket)
	if err != nil {
		t.Fatalf("error deleting ticket: %v", err)
	}

	// Nothing should be in the db.
	_, found, err := db.GetTicketByHash(ticket.Hash)
	if err != nil {
		t.Fatalf("error retrieving ticket by ticket hash: %v", err)
	}
	if found {
		t.Fatal("expected found==false")
	}
}

func testGetTicketByHash(t *testing.T) {
	// Insert a ticket into the database.
	ticket := exampleTicket()
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Retrieve ticket from database.
	retrieved, found, err := db.GetTicketByHash(ticket.Hash)
	if err != nil {
		t.Fatalf("error retrieving ticket by ticket hash: %v", err)
	}
	if !found {
		t.Fatal("expected found==true")
	}

	// Check ticket fields match expected.
	if !reflect.DeepEqual(retrieved, ticket) {
		t.Fatal("retrieved ticket value didnt match expected")
	}

	// Check found==false when requesting a non-existent ticket.
	_, found, err = db.GetTicketByHash("Not a real ticket hash")
	if err != nil {
		t.Fatalf("error retrieving ticket by ticket hash: %v", err)
	}
	if found {
		t.Fatal("expected found==false")
	}
}

func testUpdateTicket(t *testing.T) {
	ticket := exampleTicket()
	// Insert a ticket into the database.
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Update ticket with new values.
	ticket.FeeAmount = ticket.FeeAmount + 1
	ticket.FeeExpiration = ticket.FeeExpiration + 1
	ticket.VoteChoices = map[string]string{"New agenda": "New value"}

	err = db.UpdateTicket(ticket)
	if err != nil {
		t.Fatalf("error updating ticket: %v", err)
	}

	// Retrieve updated ticket from database.
	retrieved, found, err := db.GetTicketByHash(ticket.Hash)
	if err != nil {
		t.Fatalf("error retrieving ticket by ticket hash: %v", err)
	}
	if !found {
		t.Fatal("expected found==true")
	}

	if !reflect.DeepEqual(retrieved, ticket) {
		t.Fatal("retrieved ticket value didnt match expected")
	}

	// Updating a non-existent ticket should fail.
	ticket.Hash = "doesnt exist"
	err = db.UpdateTicket(ticket)
	if err == nil {
		t.Fatal("expected an error updating a ticket with non-existent hash")
	}
}

func testTicketFeeExpired(t *testing.T) {
	ticket := exampleTicket()

	now := time.Now()
	hourBefore := now.Add(-time.Hour).Unix()
	hourAfter := now.Add(time.Hour).Unix()

	ticket.FeeExpiration = hourAfter
	if ticket.FeeExpired() {
		t.Fatal("expected ticket not to be expired")
	}

	ticket.FeeExpiration = hourBefore
	if !ticket.FeeExpired() {
		t.Fatal("expected ticket to be expired")
	}
}

func testFilterTickets(t *testing.T) {
	// Insert a ticket.
	ticket := exampleTicket()
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Insert another ticket.
	ticket2 := exampleTicket()
	ticket2.Confirmed = !ticket.Confirmed
	err = db.InsertNewTicket(ticket2)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	// Expect all tickets returned.
	retrieved, err := db.filterTickets(func(t *bolt.Bucket) bool {
		return true
	})
	if err != nil {
		t.Fatalf("error filtering tickets: %v", err)
	}
	if len(retrieved) != 2 {
		t.Fatalf("expected to find 2 tickets, found %d", len(retrieved))
	}

	// Only one ticket should be confirmed.
	retrieved, err = db.filterTickets(func(t *bolt.Bucket) bool {
		return bytesToBool(t.Get(confirmedK))
	})
	if err != nil {
		t.Fatalf("error filtering tickets: %v", err)
	}
	if len(retrieved) != 1 {
		t.Fatalf("expected to find 1 ticket, found %d", len(retrieved))
	}
	if retrieved[0].Confirmed != true {
		t.Fatal("expected retrieved ticket to be confirmed")
	}

	// Expect no tickets with confirmed fee.
	retrieved, err = db.filterTickets(func(t *bolt.Bucket) bool {
		return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed
	})
	if err != nil {
		t.Fatalf("error filtering tickets: %v", err)
	}
	if len(retrieved) != 0 {
		t.Fatalf("expected to find 0 tickets, found %d", len(retrieved))
	}
}

func testCountTickets(t *testing.T) {
	count := func(test string, expectedVoting, expectedVoted, expectedRevoked int64) {
		voting, voted, revoked, err := db.CountTickets()
		if err != nil {
			t.Fatalf("error counting tickets: %v", err)
		}

		if voting != expectedVoting {
			t.Fatalf("test %s: expected %d voting tickets, got %d",
				test, expectedVoting, voting)
		}
		if voted != expectedVoted {
			t.Fatalf("test %s: expected %d voted tickets, got %d",
				test, expectedVoted, voted)
		}
		if revoked != expectedRevoked {
			t.Fatalf("test %s: expected %d revoked tickets, got %d",
				test, expectedRevoked, revoked)
		}
	}

	// Initial counts should all be zero.
	count("empty db", 0, 0, 0)

	// Insert a ticket with non-confirmed fee into the database.
	// This should not be counted.
	ticket := exampleTicket()
	ticket.FeeTxStatus = FeeReceieved
	err := db.InsertNewTicket(ticket)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	count("unconfirmed fee", 0, 0, 0)

	// Insert a ticket with confirmed fee into the database.
	// This should be counted.
	ticket2 := exampleTicket()
	ticket2.FeeTxStatus = FeeConfirmed
	err = db.InsertNewTicket(ticket2)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	count("confirmed fee", 1, 0, 0)

	// Insert a voted ticket into the database.
	// This should be counted.
	ticket3 := exampleTicket()
	ticket3.FeeTxStatus = FeeConfirmed
	ticket3.Outcome = Voted
	err = db.InsertNewTicket(ticket3)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	count("voted", 1, 1, 0)

	// Insert a revoked ticket into the database.
	// This should be counted.
	ticket4 := exampleTicket()
	ticket4.FeeTxStatus = FeeConfirmed
	ticket4.Outcome = Revoked
	err = db.InsertNewTicket(ticket4)
	if err != nil {
		t.Fatalf("error storing ticket in database: %v", err)
	}

	count("revoked", 1, 1, 1)
}
