// Copyright 2020 PingCAP, Inc. Licensed under Apache-2.0.

package export

import (
	"context"
	"errors"
	"fmt"
	"os"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap/tidb/br/pkg/storage"
	"github.com/pingcap/tidb/br/pkg/version"
	tcontext "github.com/pingcap/tidb/dumpling/context"
	"github.com/stretchr/testify/require"
)

const (
	logFile = "ON.000001"
	pos     = "7502"
	gtidSet = "6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29"
)

func TestMysqlMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos, "", "", gtidSet)
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").WillReturnError(fmt.Errorf("mock error"))
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(
		sqlmock.NewRows([]string{"exec_master_log_pos", "relay_master_log_file", "master_host", "Executed_Gtid_Set", "Seconds_Behind_Master"}))

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7502\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestMetaDataAfterConn(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos, "", "", gtidSet)
	pos2 := "7510"
	rows2 := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos2, "", "", gtidSet)
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").WillReturnError(fmt.Errorf("mock error"))
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(
		sqlmock.NewRows([]string{"exec_master_log_pos", "relay_master_log_file", "master_host", "Executed_Gtid_Set", "Seconds_Behind_Master"}))
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows2)

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, true))

	m.buffer.Write(m.afterConnBuffer.Bytes())

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7502\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n" +
		"SHOW MASTER STATUS: /* AFTER CONNECTION POOL ESTABLISHED */\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7510\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestMysqlWithFollowersMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos, "", "", gtidSet)
	followerRows := sqlmock.NewRows([]string{"exec_master_log_pos", "relay_master_log_file", "master_host", "Executed_Gtid_Set", "Seconds_Behind_Master"}).
		AddRow("256529431", "mysql-bin.001821", "192.168.1.100", gtidSet, 0)
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").WillReturnError(fmt.Errorf("mock error"))
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(followerRows)

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7502\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n" +
		"SHOW SLAVE STATUS:\n" +
		"\tHost: 192.168.1.100\n" +
		"\tLog: mysql-bin.001821\n" +
		"\tPos: 256529431\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestMysqlWithNullFollowersMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos, "", "", gtidSet)
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").WillReturnError(fmt.Errorf("mock error"))
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(sqlmock.NewRows([]string{"SQL_Remaining_Delay"}).AddRow(nil))

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7502\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestMariaDBMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	logFile := "mariadb-bin.000016"
	pos := "475"
	gtidSet := "0-1-2"
	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB"}).
		AddRow(logFile, pos, "", "")
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	rows = sqlmock.NewRows([]string{"@@global.gtid_binlog_pos"}).
		AddRow(gtidSet)
	mock.ExpectQuery("SELECT @@global.gtid_binlog_pos").WillReturnRows(rows)
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(rows)
	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMariaDB, false))
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestMariaDBWithFollowersMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set"}).
		AddRow(logFile, pos, "", "", gtidSet)
	followerRows := sqlmock.NewRows([]string{"exec_master_log_pos", "relay_master_log_file", "master_host", "Executed_Gtid_Set", "connection_name", "Seconds_Behind_Master"}).
		AddRow("256529431", "mysql-bin.001821", "192.168.1.100", gtidSet, "connection_1", 0).
		AddRow("256529451", "mysql-bin.001820", "192.168.1.102", gtidSet, "connection_2", 200)
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").
		WillReturnRows(sqlmock.NewRows([]string{"@@default_master_connection"}).
			AddRow("connection_1"))
	mock.ExpectQuery("SHOW ALL SLAVES STATUS").WillReturnRows(followerRows)

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: ON.000001\n" +
		"\tPos: 7502\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n" +
		"SHOW SLAVE STATUS:\n" +
		"\tConnection name: connection_1\n" +
		"\tHost: 192.168.1.100\n" +
		"\tLog: mysql-bin.001821\n" +
		"\tPos: 256529431\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n" +
		"SHOW SLAVE STATUS:\n" +
		"\tConnection name: connection_2\n" +
		"\tHost: 192.168.1.102\n" +
		"\tLog: mysql-bin.001820\n" +
		"\tPos: 256529451\n" +
		"\tGTID:6ce40be3-e359-11e9-87e0-36933cb0ca5a:1-29\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestEarlierMysqlMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	logFile := "mysql-bin.000001"
	pos := "4879"
	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB"}).
		AddRow(logFile, pos, "", "")
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	mock.ExpectQuery("SELECT @@default_master_connection").WillReturnError(fmt.Errorf("mock error"))
	mock.ExpectQuery("SHOW SLAVE STATUS").WillReturnRows(
		sqlmock.NewRows([]string{"exec_master_log_pos", "relay_master_log_file", "master_host", "Executed_Gtid_Set", "Seconds_Behind_Master"}))

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeMySQL, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: mysql-bin.000001\n" +
		"\tPos: 4879\n" +
		"\tGTID:\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestTiDBSnapshotMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	logFile := "tidb-binlog"
	pos := "420633329401856001"
	rows := sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB"}).
		AddRow(logFile, pos, "", "")
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeTiDB, false))

	expected := "SHOW MASTER STATUS:\n" +
		"\tLog: tidb-binlog\n" +
		"\tPos: 420633329401856001\n" +
		"\tGTID:\n\n"
	require.Equal(t, expected, m.buffer.String())

	snapshot := "420633273211289601"
	rows = sqlmock.NewRows([]string{"File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB"}).
		AddRow(logFile, pos, "", "")
	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnRows(rows)
	m = newGlobalMetadata(tcontext.Background(), createStorage(t), snapshot)
	require.NoError(t, m.recordGlobalMetaData(conn, version.ServerTypeTiDB, false))

	expected = "SHOW MASTER STATUS:\n" +
		"\tLog: tidb-binlog\n" +
		"\tPos: 420633273211289601\n" +
		"\tGTID:\n\n"
	require.Equal(t, expected, m.buffer.String())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestNoPrivilege(t *testing.T) {
	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer func() {
		require.NoError(t, db.Close())
	}()

	conn, err := db.Conn(context.Background())
	require.NoError(t, err)

	mock.ExpectQuery("SHOW MASTER STATUS").WillReturnError(errors.New("lack SUPER or REPLICATION CLIENT privilege"))

	m := newGlobalMetadata(tcontext.Background(), createStorage(t), "")
	// some consistencyType will ignore this error, this test make sure no extra message is written
	require.Error(t, m.recordGlobalMetaData(conn, version.ServerTypeTiDB, false))
	require.Equal(t, "", m.buffer.String())
}

func createStorage(t *testing.T) storage.ExternalStorage {
	backend, err := storage.ParseBackend("file:///"+os.TempDir(), nil)
	require.NoError(t, err)
	testLoc, _ := storage.Create(context.Background(), backend, true)
	return testLoc
}
