//go:build all || cassandra
// +build all cassandra

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
/*
 * Content before git sha 34fdeebefcbf183ed7f916f931aa0586fdaa1b40
 * Copyright (c) 2016, The Gocql authors,
 * provided under the BSD-3-Clause License.
 * See the NOTICE file distributed with this work for additional information.
 */

package gocql

import (
	"fmt"
	"reflect"
	"sort"
	"testing"
	"time"

	"gopkg.in/inf.v0"
)

type WikiPage struct {
	Title       string
	RevId       UUID
	Body        string
	Views       int64
	Protected   bool
	Modified    time.Time
	Rating      *inf.Dec
	Tags        []string
	Attachments map[string]WikiAttachment
}

type WikiAttachment []byte

var wikiTestData = []*WikiPage{
	{
		Title:    "Frontpage",
		RevId:    TimeUUID(),
		Body:     "Welcome to this wiki page!",
		Rating:   inf.NewDec(131, 3),
		Modified: time.Date(2013, time.August, 13, 9, 52, 3, 0, time.UTC),
		Tags:     []string{"start", "important", "test"},
		Attachments: map[string]WikiAttachment{
			"logo":    WikiAttachment("\x00company logo\x00"),
			"favicon": WikiAttachment("favicon.ico"),
		},
	},
	{
		Title:    "Foobar",
		RevId:    TimeUUID(),
		Body:     "foo::Foo f = new foo::Foo(foo::Foo::INIT);",
		Modified: time.Date(2013, time.August, 13, 9, 52, 3, 0, time.UTC),
	},
}

type WikiTest struct {
	session *Session
	tb      testing.TB

	table string
}

func CreateSchema(session *Session, tb testing.TB, table string) *WikiTest {
	table = "wiki_" + table
	if err := createTable(session, fmt.Sprintf("DROP TABLE IF EXISTS gocql_test.%s", table)); err != nil {
		tb.Fatal("CreateSchema:", err)
	}

	err := createTable(session, fmt.Sprintf(`CREATE TABLE gocql_test.%s (
			title       varchar,
			revid       timeuuid,
			body        varchar,
			views       bigint,
			protected   boolean,
			modified    timestamp,
			rating      decimal,
			tags        set<varchar>,
			attachments map<varchar, blob>,
			PRIMARY KEY (title, revid)
		)`, table))

	if err != nil {
		tb.Fatal("CreateSchema:", err)
	}

	return &WikiTest{
		session: session,
		tb:      tb,
		table:   table,
	}
}

func (w *WikiTest) CreatePages(n int) {
	var page WikiPage
	t0 := time.Now()
	for i := 0; i < n; i++ {
		page.Title = fmt.Sprintf("generated_%d", (i&16)+1)
		page.Modified = t0.Add(time.Duration(i-n) * time.Minute)
		page.RevId = UUIDFromTime(page.Modified)
		page.Body = fmt.Sprintf("text %d", i)
		if err := w.InsertPage(&page); err != nil {
			w.tb.Error("CreatePages:", err)
		}
	}
}

func (w *WikiTest) InsertPage(page *WikiPage) error {
	return w.session.Query(fmt.Sprintf(`INSERT INTO %s
		(title, revid, body, views, protected, modified, rating, tags, attachments)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, w.table),
		page.Title, page.RevId, page.Body, page.Views, page.Protected,
		page.Modified, page.Rating, page.Tags, page.Attachments).Exec()
}

func (w *WikiTest) SelectPage(page *WikiPage, title string, revid UUID) error {
	return w.session.Query(fmt.Sprintf(`SELECT title, revid, body, views, protected,
		modified,tags, attachments, rating
		FROM %s WHERE title = ? AND revid = ? LIMIT 1`, w.table),
		title, revid).Scan(&page.Title, &page.RevId,
		&page.Body, &page.Views, &page.Protected, &page.Modified, &page.Tags,
		&page.Attachments, &page.Rating)
}

func (w *WikiTest) GetPageCount() int {
	var count int
	if err := w.session.Query(fmt.Sprintf(`SELECT COUNT(*) FROM %s`, w.table)).Scan(&count); err != nil {
		w.tb.Error("GetPageCount", err)
	}
	return count
}

func TestWikiCreateSchema(t *testing.T) {
	session := createSession(t)
	defer session.Close()

	CreateSchema(session, t, "create")
}

func BenchmarkWikiCreateSchema(b *testing.B) {
	b.StopTimer()
	session := createSession(b)
	defer func() {
		b.StopTimer()
		session.Close()
	}()

	b.StartTimer()
	for i := 0; i < b.N; i++ {
		CreateSchema(session, b, "bench_create")
	}
}

func TestWikiCreatePages(t *testing.T) {
	session := createSession(t)
	defer session.Close()

	w := CreateSchema(session, t, "create_pages")

	numPages := 5
	w.CreatePages(numPages)
	if count := w.GetPageCount(); count != numPages {
		t.Errorf("expected %d pages, got %d pages.", numPages, count)
	}
}

func BenchmarkWikiCreatePages(b *testing.B) {
	b.StopTimer()
	session := createSession(b)
	defer func() {
		b.StopTimer()
		session.Close()
	}()

	w := CreateSchema(session, b, "bench_create_pages")

	b.StartTimer()

	w.CreatePages(b.N)
}

func BenchmarkWikiSelectAllPages(b *testing.B) {
	b.StopTimer()
	session := createSession(b)
	defer func() {
		b.StopTimer()
		session.Close()
	}()
	w := CreateSchema(session, b, "bench_select_all")

	w.CreatePages(100)
	b.StartTimer()

	var page WikiPage
	for i := 0; i < b.N; i++ {
		iter := session.Query(fmt.Sprintf(`SELECT title, revid, body, views, protected,
			modified, tags, attachments, rating
			FROM %s`, w.table)).Iter()
		for iter.Scan(&page.Title, &page.RevId, &page.Body, &page.Views,
			&page.Protected, &page.Modified, &page.Tags, &page.Attachments,
			&page.Rating) {
			// pass
		}
		if err := iter.Close(); err != nil {
			b.Error(err)
		}
	}
}

func BenchmarkWikiSelectSinglePage(b *testing.B) {
	b.StopTimer()
	session := createSession(b)
	defer func() {
		b.StopTimer()
		session.Close()
	}()
	w := CreateSchema(session, b, "bench_select_single")
	pages := make([]WikiPage, 100)
	w.CreatePages(len(pages))
	iter := session.Query(fmt.Sprintf(`SELECT title, revid FROM %s`, w.table)).Iter()
	for i := 0; i < len(pages); i++ {
		if !iter.Scan(&pages[i].Title, &pages[i].RevId) {
			pages = pages[:i]
			break
		}
	}
	if err := iter.Close(); err != nil {
		b.Error(err)
	}
	b.StartTimer()

	var page WikiPage
	for i := 0; i < b.N; i++ {
		p := &pages[i%len(pages)]
		if err := w.SelectPage(&page, p.Title, p.RevId); err != nil {
			b.Error(err)
		}
	}
}

func BenchmarkWikiSelectPageCount(b *testing.B) {
	b.StopTimer()
	session := createSession(b)
	defer func() {
		b.StopTimer()
		session.Close()
	}()

	w := CreateSchema(session, b, "bench_page_count")
	const numPages = 10
	w.CreatePages(numPages)
	b.StartTimer()
	for i := 0; i < b.N; i++ {
		if count := w.GetPageCount(); count != numPages {
			b.Errorf("expected %d pages, got %d pages.", numPages, count)
		}
	}
}

func TestWikiTypicalCRUD(t *testing.T) {
	session := createSession(t)
	defer session.Close()

	w := CreateSchema(session, t, "crud")

	for _, page := range wikiTestData {
		if err := w.InsertPage(page); err != nil {
			t.Error("InsertPage:", err)
		}
	}
	if count := w.GetPageCount(); count != len(wikiTestData) {
		t.Errorf("count: expected %d, got %d\n", len(wikiTestData), count)
	}
	for _, original := range wikiTestData {
		page := new(WikiPage)
		if err := w.SelectPage(page, original.Title, original.RevId); err != nil {
			t.Error("SelectPage:", err)
			continue
		}
		sort.Sort(sort.StringSlice(page.Tags))
		sort.Sort(sort.StringSlice(original.Tags))
		if !reflect.DeepEqual(page, original) {
			t.Errorf("page: expected %#v, got %#v\n", original, page)
		}
	}
}
