// VulcanizeDB
// Copyright © 2019 Vulcanize

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package event_test

import (
	"fmt"
	"math/big"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
	"github.com/vulcanize/vulcanizedb/libraries/shared/test_data"
	"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/test_config"
)

var _ = Describe("Repository", func() {
	var db *postgres.DB

	BeforeEach(func() {
		db = test_config.NewTestDB(test_config.NewTestNode())
		test_config.CleanTestDB(db)
	})

	Describe("Create", func() {
		const createTestEventTableQuery = `CREATE TABLE public.testEvent(
		id        SERIAL PRIMARY KEY,
		header_id INTEGER NOT NULL REFERENCES headers (id) ON DELETE CASCADE,
		log_id    BIGINT  NOT NULL REFERENCES header_sync_logs (id) ON DELETE CASCADE,
		variable1 TEXT,
		UNIQUE (header_id, log_id)
		);`

		var (
			headerID, logID  int64
			headerRepository repositories.HeaderRepository
			testModel        event.InsertionModel
		)

		BeforeEach(func() {
			_, tableErr := db.Exec(createTestEventTableQuery)
			Expect(tableErr).NotTo(HaveOccurred())
			headerRepository = repositories.NewHeaderRepository(db)
			var insertHeaderErr error
			headerID, insertHeaderErr = headerRepository.CreateOrUpdateHeader(fakes.FakeHeader)
			Expect(insertHeaderErr).NotTo(HaveOccurred())
			headerSyncLog := test_data.CreateTestLog(headerID, db)
			logID = headerSyncLog.ID

			testModel = event.InsertionModel{
				SchemaName: "public",
				TableName:  "testEvent",
				OrderedColumns: []event.ColumnName{
					event.HeaderFK, event.LogFK, "variable1",
				},
				ColumnValues: event.ColumnValues{
					event.HeaderFK: headerID,
					event.LogFK:    logID,
					"variable1":    "value1",
				},
			}
		})

		AfterEach(func() {
			db.MustExec(`DROP TABLE public.testEvent;`)
		})

		// Needs to run before the other tests, since those insert keys in map
		It("memoizes queries", func() {
			Expect(len(event.ModelToQuery)).To(Equal(0))
			event.GetMemoizedQuery(testModel)
			Expect(len(event.ModelToQuery)).To(Equal(1))
			event.GetMemoizedQuery(testModel)
			Expect(len(event.ModelToQuery)).To(Equal(1))
		})

		It("persists a model to postgres", func() {
			createErr := event.Create([]event.InsertionModel{testModel}, db)
			Expect(createErr).NotTo(HaveOccurred())

			var res TestEvent
			dbErr := db.Get(&res, `SELECT log_id, variable1 FROM public.testEvent;`)
			Expect(dbErr).NotTo(HaveOccurred())

			Expect(res.LogID).To(Equal(fmt.Sprint(testModel.ColumnValues[event.LogFK])))
			Expect(res.Variable1).To(Equal(testModel.ColumnValues["variable1"]))
		})

		Describe("returns errors", func() {
			It("for empty model slice", func() {
				err := event.Create([]event.InsertionModel{}, db)
				Expect(err).To(MatchError("repository got empty model slice"))
			})

			It("for failed SQL inserts", func() {
				header := fakes.GetFakeHeader(1)
				headerID, headerErr := headerRepository.CreateOrUpdateHeader(header)
				Expect(headerErr).NotTo(HaveOccurred())

				brokenModel := event.InsertionModel{
					SchemaName: "public",
					TableName:  "testEvent",
					// Wrong name of last column compared to DB, will generate incorrect query
					OrderedColumns: []event.ColumnName{
						event.HeaderFK, event.LogFK, "variable2",
					},
					ColumnValues: event.ColumnValues{
						event.HeaderFK: headerID,
						event.LogFK:    logID,
						"variable1":    "value1",
					},
				}

				// Remove cached queries, or we won't generate a new (incorrect) one
				delete(event.ModelToQuery, "publictestEvent")

				createErr := event.Create([]event.InsertionModel{brokenModel}, db)
				// Remove incorrect query, so other tests won't get it
				delete(event.ModelToQuery, "publictestEvent")

				Expect(createErr).To(HaveOccurred())
			})

			It("for unsupported types in ColumnValue", func() {
				unsupportedValue := big.NewInt(5)
				testModel = event.InsertionModel{
					SchemaName: "public",
					TableName:  "testEvent",
					OrderedColumns: []event.ColumnName{
						event.HeaderFK, event.LogFK, "variable1",
					},
					ColumnValues: event.ColumnValues{
						event.HeaderFK: headerID,
						event.LogFK:    logID,
						"variable1":    unsupportedValue,
					},
				}

				createErr := event.Create([]event.InsertionModel{testModel}, db)
				Expect(createErr).To(MatchError(event.ErrUnsupportedValue(unsupportedValue)))
			})
		})

		It("upserts queries with conflicting source", func() {
			conflictingModel := event.InsertionModel{
				SchemaName: "public",
				TableName:  "testEvent",
				OrderedColumns: []event.ColumnName{
					event.HeaderFK, event.LogFK, "variable1",
				},
				ColumnValues: event.ColumnValues{
					event.HeaderFK: headerID,
					event.LogFK:    logID,
					"variable1":    "conflictingValue",
				},
			}

			createErr := event.Create([]event.InsertionModel{testModel, conflictingModel}, db)
			Expect(createErr).NotTo(HaveOccurred())

			var res TestEvent
			dbErr := db.Get(&res, `SELECT log_id, variable1 FROM public.testEvent;`)
			Expect(dbErr).NotTo(HaveOccurred())
			Expect(res.Variable1).To(Equal(conflictingModel.ColumnValues["variable1"]))
		})

		It("generates correct queries", func() {
			actualQuery := event.GenerateInsertionQuery(testModel)
			expectedQuery := `INSERT INTO public.testEvent (header_id, log_id, variable1) VALUES($1, $2, $3)
		ON CONFLICT (header_id, log_id) DO UPDATE SET header_id = $1, log_id = $2, variable1 = $3;`
			Expect(actualQuery).To(Equal(expectedQuery))
		})

		It("marks log transformed", func() {
			createErr := event.Create([]event.InsertionModel{testModel}, db)
			Expect(createErr).NotTo(HaveOccurred())

			var logTransformed bool
			getErr := db.Get(&logTransformed, `SELECT transformed FROM public.header_sync_logs WHERE id = $1`, logID)
			Expect(getErr).NotTo(HaveOccurred())
			Expect(logTransformed).To(BeTrue())
		})
	})
})

type TestEvent struct {
	LogID     string `db:"log_id"`
	Variable1 string
}
