// Copyright 2018 Vulcanize
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package flip_kick_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/vulcanize/vulcanizedb/pkg/core"
	"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
	"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories"
	"github.com/vulcanize/vulcanizedb/pkg/fakes"
	"github.com/vulcanize/vulcanizedb/pkg/transformers/flip_kick"
	"github.com/vulcanize/vulcanizedb/pkg/transformers/test_data"
	"github.com/vulcanize/vulcanizedb/test_config"
)

var _ = Describe("FlipKick Repository", func() {
	var db *postgres.DB
	var flipKickRepository flip_kick.FlipKickRepository
	var headerId int64
	var blockNumber int64
	var flipKick = test_data.FlipKickModel

	BeforeEach(func() {
		db = test_config.NewTestDB(test_config.NewTestNode())
		test_config.CleanTestDB(db)
		flipKickRepository = flip_kick.FlipKickRepository{}
		flipKickRepository.SetDB(db)
		blockNumber = GinkgoRandomSeed()
		headerId = createHeader(db, blockNumber)

		_, err := db.Exec(`DELETE from maker.flip_kick;`)
		Expect(err).NotTo(HaveOccurred())
	})

	Describe("Create", func() {
		AfterEach(func() {
			_, err := db.Exec(`DELETE from headers`)
			Expect(err).NotTo(HaveOccurred())
		})

		It("persists flip_kick records", func() {
			err := flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).NotTo(HaveOccurred())

			assertDBRecordCount(db, "maker.flip_kick", 1)

			dbResult := test_data.FlipKickDBRow{}
			err = db.QueryRowx(`SELECT * FROM maker.flip_kick`).StructScan(&dbResult)
			Expect(err).NotTo(HaveOccurred())
			Expect(dbResult.HeaderId).To(Equal(headerId))
			Expect(dbResult.BidId).To(Equal(flipKick.BidId))
			Expect(dbResult.Lot).To(Equal(flipKick.Lot))
			Expect(dbResult.Bid).To(Equal(flipKick.Bid))
			Expect(dbResult.Gal).To(Equal(flipKick.Gal))
			Expect(dbResult.End.Equal(flipKick.End)).To(BeTrue())
			Expect(dbResult.Urn).To(Equal(flipKick.Urn))
			Expect(dbResult.Tab).To(Equal(flipKick.Tab))
			Expect(dbResult.TransactionIndex).To(Equal(flipKick.TransactionIndex))
			Expect(dbResult.LogIndex).To(Equal(flipKick.LogIndex))
			Expect(dbResult.Raw).To(MatchJSON(flipKick.Raw))
		})

		It("marks header checked", func() {
			err := flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).NotTo(HaveOccurred())

			var headerChecked bool
			err = db.Get(&headerChecked, `SELECT flip_kick_checked FROM public.checked_headers WHERE header_id = $1`, headerId)
			Expect(err).NotTo(HaveOccurred())
			Expect(headerChecked).To(BeTrue())
		})

		It("updates the header to checked if checked headers row already exists", func() {
			_, err := db.Exec(`INSERT INTO public.checked_headers (header_id) VALUES ($1)`, headerId)
			Expect(err).NotTo(HaveOccurred())
			err = flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).NotTo(HaveOccurred())

			var headerChecked bool
			err = db.Get(&headerChecked, `SELECT flip_kick_checked FROM public.checked_headers WHERE header_id = $1`, headerId)
			Expect(err).NotTo(HaveOccurred())
			Expect(headerChecked).To(BeTrue())
		})

		It("returns an error if inserting the flip_kick record fails", func() {
			err := flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).NotTo(HaveOccurred())

			err = flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("pq: duplicate key value violates unique constraint"))
		})

		It("allows for multiple flip kick events in one transaction if they have different log indexes", func() {
			newFlipKick := test_data.FlipKickModel
			newFlipKick.LogIndex = newFlipKick.LogIndex + 1
			err := flipKickRepository.Create(headerId, []interface{}{newFlipKick})

			Expect(err).NotTo(HaveOccurred())
		})

		It("deletes the flip_kick records if its corresponding header record is deleted", func() {
			err := flipKickRepository.Create(headerId, []interface{}{flipKick})
			Expect(err).NotTo(HaveOccurred())
			assertDBRecordCount(db, "maker.flip_kick", 1)
			assertDBRecordCount(db, "headers", 1)

			_, err = db.Exec(`DELETE FROM headers where id = $1`, headerId)
			Expect(err).NotTo(HaveOccurred())

			assertDBRecordCount(db, "headers", 0)
			assertDBRecordCount(db, "maker.flip_kick", 0)
		})

		It("returns an error if the wrong model type is passed in", func() {
			err := flipKickRepository.Create(headerId, []interface{}{test_data.WrongModel{}})

			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("model of type"))
		})
	})

	Describe("MarkHeaderChecked", func() {
		It("creates a row for a new headerID", func() {
			err := flipKickRepository.MarkHeaderChecked(headerId)

			Expect(err).NotTo(HaveOccurred())
			var headerChecked bool
			err = db.Get(&headerChecked, `SELECT flip_kick_checked FROM public.checked_headers WHERE header_id = $1`, headerId)
			Expect(err).NotTo(HaveOccurred())
			Expect(headerChecked).To(BeTrue())
		})

		It("updates row when headerID already exists", func() {
			_, err := db.Exec(`INSERT INTO public.checked_headers (header_id) VALUES ($1)`, headerId)
			Expect(err).NotTo(HaveOccurred())

			err = flipKickRepository.MarkHeaderChecked(headerId)

			Expect(err).NotTo(HaveOccurred())
			var headerChecked bool
			err = db.Get(&headerChecked, `SELECT flip_kick_checked FROM public.checked_headers WHERE header_id = $1`, headerId)
			Expect(err).NotTo(HaveOccurred())
			Expect(headerChecked).To(BeTrue())
		})
	})

	Describe("When there are multiple nodes", func() {
		var db2 *postgres.DB
		var flipKickRepository2 flip_kick.FlipKickRepository

		BeforeEach(func() {
			//create database for the second node
			node2 := core.Node{
				GenesisBlock: "GENESIS",
				NetworkID:    1,
				ID:           "node2",
				ClientName:   "Geth/v1.7.2-stable-1db4ecdc/darwin-amd64/go1.9",
			}
			db2 = test_config.NewTestDB(node2)
			flipKickRepository2 = flip_kick.FlipKickRepository{}
			flipKickRepository2.SetDB(db2)
			createHeader(db2, blockNumber)

			_, err := db2.Exec(`DELETE from maker.flip_kick;`)
			Expect(err).NotTo(HaveOccurred())
		})

		It("only includes missing headers for the current node", func() {
			node1missingHeaders, err := flipKickRepository.MissingHeaders(blockNumber, blockNumber)
			Expect(err).NotTo(HaveOccurred())
			Expect(len(node1missingHeaders)).To(Equal(1))

			node2MissingHeaders, err := flipKickRepository2.MissingHeaders(blockNumber, blockNumber)
			Expect(err).NotTo(HaveOccurred())
			Expect(len(node2MissingHeaders)).To(Equal(1))
		})
	})

	Describe("MissingHeaders", func() {
		It("returns headers that haven't been marked as checked", func() {
			startingBlock := blockNumber - 3
			endingBlock := blockNumber + 3
			err := flipKickRepository.MarkHeaderChecked(headerId)
			Expect(err).NotTo(HaveOccurred())

			newBlockNumber := blockNumber + 3
			newHeaderId := createHeader(db, newBlockNumber)
			createHeader(db, blockNumber+10) //this one is out of the block range and shouldn't be included
			headers, err := flipKickRepository.MissingHeaders(startingBlock, endingBlock)
			Expect(len(headers)).To(Equal(1))
			Expect(headers[0].Id).To(Equal(newHeaderId))
			Expect(headers[0].BlockNumber).To(Equal(newBlockNumber))
		})
	})
})

func assertDBRecordCount(db *postgres.DB, dbTable string, expectedCount int) {
	var count int
	query := `SELECT count(*) FROM ` + dbTable
	err := db.QueryRow(query).Scan(&count)
	Expect(err).NotTo(HaveOccurred())
	Expect(count).To(Equal(expectedCount))
}

func createHeader(db *postgres.DB, blockNumber int64) (headerId int64) {
	headerRepository := repositories.NewHeaderRepository(db)
	_, err := headerRepository.CreateOrUpdateHeader(fakes.GetFakeHeader(blockNumber))
	Expect(err).NotTo(HaveOccurred())

	var dbHeader core.Header
	err = db.Get(&dbHeader, `SELECT id, block_number, hash, raw FROM public.headers WHERE block_number = $1 AND eth_node_id = $2`, blockNumber, db.NodeID)
	Expect(err).NotTo(HaveOccurred())
	return dbHeader.Id
}
