//go:build go1.8

/*
 * 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.
 */

package avatica

import (
	"database/sql"
	"math"
	"reflect"
	"testing"
	"time"
)

func TestHSQLDBContext(t *testing.T) {

	skipTestIfNotHSQLDB(t)

	runTests(t, dsn, func(dbt *DBTest) {

		// Create and seed table
		ctx, cancel := getContext()
		defer cancel()

		dbt.mustExecContext(ctx, "CREATE TABLE "+dbt.tableName+" (id BIGINT PRIMARY KEY, val VARCHAR(1))")

		dbt.mustExecContext(ctx, "INSERT INTO "+dbt.tableName+" VALUES (1,'A')")

		dbt.mustExecContext(ctx, "INSERT INTO "+dbt.tableName+" VALUES (2,'B')")

		rows := dbt.mustQueryContext(ctx, "SELECT COUNT(*) FROM "+dbt.tableName)
		defer rows.Close()

		for rows.Next() {

			var count int

			err := rows.Scan(&count)

			if err != nil {
				dbt.Fatal(err)
			}

			if count != 2 {
				dbt.Fatalf("There should be 2 rows, got %d", count)
			}
		}

		// Test transactions and prepared statements
		_, err := dbt.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadUncommitted, ReadOnly: true})

		if err == nil {
			t.Error("Expected an error while creating a read only transaction, but no error was returned")
		}

		tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

		if err != nil {
			t.Errorf("Unexpected error while creating transaction: %s", err)
		}

		stmt, err := tx.PrepareContext(ctx, "INSERT INTO "+dbt.tableName+" VALUES(?,?)")

		if err != nil {
			t.Errorf("Unexpected error while preparing statement: %s", err)
		}

		res, err := stmt.ExecContext(ctx, 3, "C")

		if err != nil {
			t.Errorf("Unexpected error while executing statement: %s", err)
		}

		affected, err := res.RowsAffected()

		if err != nil {
			t.Errorf("Error getting affected rows: %s", err)
		}

		if affected != 1 {
			t.Errorf("Expected 1 affected row, got %d", affected)
		}

		err = tx.Commit()

		if err != nil {
			t.Errorf("Error committing transaction: %s", err)
		}

		stmt2, err := dbt.db.PrepareContext(ctx, "SELECT * FROM "+dbt.tableName+" WHERE id = ?")

		if err != nil {
			t.Errorf("Error preparing statement: %s", err)
		}

		row := stmt2.QueryRowContext(ctx, 3)

		if err != nil {
			t.Errorf("Error querying for row: %s", err)
		}

		var (
			queryID  int64
			queryVal string
		)

		err = row.Scan(&queryID, &queryVal)

		if err != nil {
			t.Errorf("Error scanning results into variable: %s", err)
		}

		if queryID != 3 {
			t.Errorf("Expected scanned id to be %d, got %d", 3, queryID)
		}

		if queryVal != "C" {
			t.Errorf("Expected scanned string to be %s, got %s", "C", queryVal)
		}
	})
}

func TestHSQLDBMultipleResultSets(t *testing.T) {

	skipTestIfNotHSQLDB(t)

	runTests(t, dsn, func(dbt *DBTest) {
		// Create and seed table
		ctx, cancel := getContext()
		defer cancel()

		dbt.mustExecContext(ctx, "CREATE TABLE "+dbt.tableName+" (id BIGINT PRIMARY KEY, val VARCHAR(1))")

		dbt.mustExecContext(ctx, "INSERT INTO "+dbt.tableName+" VALUES (1,'A')")

		dbt.mustExecContext(ctx, "INSERT INTO "+dbt.tableName+" VALUES (2,'B')")

		rows, err := dbt.db.QueryContext(ctx, "SELECT * FROM "+dbt.tableName+" WHERE id = 1")

		if err != nil {
			t.Errorf("Unexpected error while executing query: %s", err)
		}

		defer rows.Close()

		for rows.Next() {
			var (
				id  int64
				val string
			)

			if err := rows.Scan(&id, &val); err != nil {
				t.Errorf("Error while scanning row into variables: %s", err)
			}

			if id != 1 {
				t.Errorf("Expected id to be %d, got %d", 1, id)
			}

			if val != "A" {
				t.Errorf("Expected value to be %s, got %s", "A", val)
			}
		}

		if rows.NextResultSet() {
			t.Error("There should be no more result sets, but got another result set")
		}
	})
}

func TestHSQLDBColumnTypes(t *testing.T) {

	skipTestIfNotHSQLDB(t)

	runTests(t, dsn, func(dbt *DBTest) {

		// Create and seed table
		dbt.mustExec(`CREATE TABLE ` + dbt.tableName + ` (
				int INTEGER PRIMARY KEY,
				bint BIGINT,
				tint TINYINT,
				sint SMALLINT,
				dbl DOUBLE,
				dec DECIMAL(10, 5),
				dec2 DECIMAL,
				bool BOOLEAN,
				tm TIME,
				dt DATE,
				tmstmp TIMESTAMP,
				var VARCHAR(10),
				ch CHAR(3),
				bin BINARY(20),
				varbin VARBINARY(20)
			    )`)

		// Select
		ctx, cancel := getContext()
		defer cancel()

		rows, err := dbt.db.QueryContext(ctx, "SELECT * FROM "+dbt.tableName)

		if err != nil {
			t.Errorf("Unexpected error while selecting from table: %s", err)
		}

		columnNames, err := rows.Columns()

		if err != nil {
			t.Errorf("Error getting column names: %s", err)
		}

		expectedColumnNames := []string{"INT", "BINT", "TINT", "SINT", "DBL", "DEC", "DEC2", "BOOL", "TM", "DT", "TMSTMP", "VAR", "CH", "BIN", "VARBIN"}

		if !reflect.DeepEqual(columnNames, expectedColumnNames) {
			t.Error("Column names does not match expected column names")
		}

		type decimalSize struct {
			precision int64
			scale     int64
			ok        bool
		}

		type length struct {
			length int64
			ok     bool
		}

		type nullable struct {
			nullable bool
			ok       bool
		}

		expectedColumnTypes := []struct {
			databaseTypeName string
			decimalSize      decimalSize
			length           length
			name             string
			nullable         nullable
			scanType         reflect.Type
		}{
			{
				databaseTypeName: "INTEGER",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "INT",
				nullable: nullable{
					nullable: false,
					ok:       true,
				},
				scanType: reflect.TypeOf(int64(0)),
			},
			{
				databaseTypeName: "BIGINT",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "BINT",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(int64(0)),
			},
			{
				databaseTypeName: "TINYINT",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "TINT",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(int64(0)),
			},
			{
				databaseTypeName: "SMALLINT",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "SINT",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(int64(0)),
			},
			{
				databaseTypeName: "DOUBLE",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "DBL",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(float64(0)),
			},
			{
				databaseTypeName: "DECIMAL",
				decimalSize: decimalSize{
					precision: 10,
					scale:     5,
					ok:        true,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "DEC",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(""),
			},
			{
				databaseTypeName: "DECIMAL",
				decimalSize: decimalSize{
					precision: 128,
					scale:     math.MaxInt64,
					ok:        true,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "DEC2",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(""),
			},
			{
				databaseTypeName: "BOOLEAN",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "BOOL",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(false),
			},
			{
				databaseTypeName: "TIME",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "TM",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(time.Time{}),
			},
			{
				databaseTypeName: "DATE",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "DT",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(time.Time{}),
			},
			{
				databaseTypeName: "TIMESTAMP",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 0,
					ok:     false,
				},
				name: "TMSTMP",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(time.Time{}),
			},
			{
				databaseTypeName: "VARCHAR",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 10,
					ok:     true,
				},
				name: "VAR",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(""),
			},
			{
				databaseTypeName: "CHARACTER",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 3,
					ok:     true,
				},
				name: "CH",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf(""),
			},
			{
				databaseTypeName: "BINARY",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 20,
					ok:     true,
				},
				name: "BIN",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf([]byte{}),
			},
			{
				databaseTypeName: "VARBINARY",
				decimalSize: decimalSize{
					precision: 0,
					scale:     0,
					ok:        false,
				},
				length: length{
					length: 20,
					ok:     true,
				},
				name: "VARBIN",
				nullable: nullable{
					nullable: true,
					ok:       true,
				},
				scanType: reflect.TypeOf([]byte{}),
			},
		}

		columnTypes, err := rows.ColumnTypes()

		if err != nil {
			t.Errorf("Error getting column types: %s", err)
		}

		for index, columnType := range columnTypes {

			expected := expectedColumnTypes[index]

			if columnType.DatabaseTypeName() != expected.databaseTypeName {
				t.Errorf("Expected database type name for index %d to be %s, got %s", index, expected.databaseTypeName, columnType.DatabaseTypeName())
			}

			precision, scale, ok := columnType.DecimalSize()

			if precision != expected.decimalSize.precision {
				t.Errorf("Expected decimal precision for index %d to be %d, got %d", index, expected.decimalSize.precision, precision)
			}

			if scale != expected.decimalSize.scale {
				t.Errorf("Expected decimal scale for index %d to be %d, got %d", index, expected.decimalSize.scale, scale)
			}

			if ok != expected.decimalSize.ok {
				t.Errorf("Expected decimal ok for index %d to be %t, got %t", index, expected.decimalSize.ok, ok)
			}

			length, ok := columnType.Length()

			if length != expected.length.length {
				t.Errorf("Expected length for index %d to be %d, got %d", index, expected.length.length, length)
			}

			if ok != expected.length.ok {
				t.Errorf("Expected length ok for index %d to be %t, got %t", index, expected.length.ok, ok)
			}

			if columnType.Name() != expected.name {
				t.Errorf("Expected column name for index %d to be %s, got %s", index, expected.name, columnType.Name())
			}

			nullable, ok := columnType.Nullable()

			if nullable != expected.nullable.nullable {
				t.Errorf("Expected nullable for index %d to be %t, got %t", index, expected.nullable.nullable, nullable)
			}

			if ok != expected.nullable.ok {
				t.Errorf("Expected nullable ok for index %d to be %t, got %t", index, expected.nullable.ok, ok)
			}

			if columnType.ScanType() != expected.scanType {
				t.Errorf("Expected scan type for index %d to be %s, got %s", index, expected.scanType, columnType.ScanType())
			}
		}

	})
}
