package dbconnect_tests

import (
	"context"
	"log"
	"net/url"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/cloudflare/cloudflared/dbconnect"
)

func TestIntegrationPostgreSQL(t *testing.T) {
	ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")

	err := pq.Ping(ctx)
	assert.NoError(t, err)

	_, err = pq.Submit(ctx, &dbconnect.Command{
		Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	_, err = pq.Submit(ctx, &dbconnect.Command{
		Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
		Mode:      "exec",
		Arguments: dbconnect.Arguments{
			Positional: []interface{}{
				"text",
				"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
				"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
				[]string{"1.1.1.1", "1.0.0.1"},
			},
		},
	})
	assert.NoError(t, err)

	_, err = pq.Submit(ctx, &dbconnect.Command{
		Statement: "UPDATE t SET b = NULL;",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	res, err := pq.Submit(ctx, &dbconnect.Command{
		Statement: "SELECT * FROM t;",
		Mode:      "query",
	})
	assert.NoError(t, err)
	assert.IsType(t, make([]map[string]interface{}, 0), res)

	actual := res.([]map[string]interface{})[0]
	expected := map[string]interface{}{
		"a": "text",
		"b": nil,
		"c": map[string]interface{}{
			"bool":  true,
			"array": []interface{}{"a", float64(1), 3.14},
			"embed": map[string]interface{}{"num": float64(21)},
		},
		"d": "{1.1.1.1,1.0.0.1}",
		"e": int64(1),
	}
	assert.EqualValues(t, expected, actual)

	_, err = pq.Submit(ctx, &dbconnect.Command{
		Statement: "DROP TABLE t;",
		Mode:      "exec",
	})
	assert.NoError(t, err)
}

func TestIntegrationMySQL(t *testing.T) {
	ctx, my := helperNewSQLClient(t, "MYSQL_URL")

	err := my.Ping(ctx)
	assert.NoError(t, err)

	_, err = my.Submit(ctx, &dbconnect.Command{
		Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	_, err = my.Submit(ctx, &dbconnect.Command{
		Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
		Mode:      "exec",
		Arguments: dbconnect.Arguments{
			Positional: []interface{}{
				"a",
				10,
				3.14,
				"{\"bool\":true}",
				2000,
			},
		},
	})
	assert.NoError(t, err)

	_, err = my.Submit(ctx, &dbconnect.Command{
		Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	res, err := my.Submit(ctx, &dbconnect.Command{
		Statement: "SELECT * FROM t;",
		Mode:      "query",
	})
	assert.NoError(t, err)
	assert.IsType(t, make([]map[string]interface{}, 0), res)

	actual := res.([]map[string]interface{})[0]
	expected := map[string]interface{}{
		"a": "a",
		"b": float64(10),
		"c": 3.14,
		"d": map[string]interface{}{"bool": true},
		"e": float64(2000),
		"f": nil,
	}
	assert.EqualValues(t, expected, actual)

	_, err = my.Submit(ctx, &dbconnect.Command{
		Statement: "DROP TABLE t;",
		Mode:      "exec",
	})
	assert.NoError(t, err)
}

func TestIntegrationMSSQL(t *testing.T) {
	ctx, ms := helperNewSQLClient(t, "MSSQL_URL")

	err := ms.Ping(ctx)
	assert.NoError(t, err)

	_, err = ms.Submit(ctx, &dbconnect.Command{
		Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
		Mode:      "exec"})
	assert.NoError(t, err)

	_, err = ms.Submit(ctx, &dbconnect.Command{
		Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
		Mode:      "exec",
		Arguments: dbconnect.Arguments{
			Positional: []interface{}{
				0,
				3,
				"$0.99",
				"text",
			},
		},
	})
	assert.NoError(t, err)

	_, err = ms.Submit(ctx, &dbconnect.Command{
		Statement: "UPDATE t SET d = NULL;",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	res, err := ms.Submit(ctx, &dbconnect.Command{
		Statement: "SELECT * FROM t;",
		Mode:      "query",
	})
	assert.NoError(t, err)
	assert.IsType(t, make([]map[string]interface{}, 0), res)

	actual := res.([]map[string]interface{})[0]
	expected := map[string]interface{}{
		"a": false,
		"b": float64(3),
		"c": float64(0.99),
		"d": nil,
	}
	assert.EqualValues(t, expected, actual)

	_, err = ms.Submit(ctx, &dbconnect.Command{
		Statement: "DROP TABLE t;",
		Mode:      "exec",
	})
	assert.NoError(t, err)
}

func TestIntegrationClickhouse(t *testing.T) {
	ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")

	err := ch.Ping(ctx)
	assert.NoError(t, err)

	_, err = ch.Submit(ctx, &dbconnect.Command{
		Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
		Mode:      "exec",
	})
	assert.NoError(t, err)

	_, err = ch.Submit(ctx, &dbconnect.Command{
		Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
		Mode:      "exec",
		Arguments: dbconnect.Arguments{
			Positional: []interface{}{
				"ec65f626-6f50-4c86-9628-6314ef1edacd",
				"",
				3.14,
				314,
				-144,
				[]string{"a", "b", "c"},
			},
		},
	})
	assert.NoError(t, err)

	res, err := ch.Submit(ctx, &dbconnect.Command{
		Statement: "SELECT * FROM t;",
		Mode:      "query",
	})
	assert.NoError(t, err)
	assert.IsType(t, make([]map[string]interface{}, 0), res)

	actual := res.([]map[string]interface{})[0]
	expected := map[string]interface{}{
		"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
		"b": "",
		"c": float64(3.14),
		"d": uint32(314),
		"e": int16(-144),
		"f": []string{"a", "b", "c"},
	}
	assert.EqualValues(t, expected, actual)

	_, err = ch.Submit(ctx, &dbconnect.Command{
		Statement: "DROP TABLE t;",
		Mode:      "exec",
	})
	assert.NoError(t, err)
}

func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
	_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
	if ok {
		t.Helper()
	} else {
		t.SkipNow()
	}

	val, ok := os.LookupEnv(env)
	if !ok {
		log.Fatalf("must provide database url as environment variable: %s", env)
	}

	parsed, err := url.Parse(val)
	if err != nil {
		log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
	}

	ctx := context.Background()
	client, err := dbconnect.NewSQLClient(ctx, parsed)
	if err != nil {
		log.Fatalf("could not start test client: %s", err)
	}

	return ctx, client
}
