package dnsprovider

import (
	"encoding/json"
	"regexp"
	"testing"

	"npm/internal/model"
	"npm/internal/test"
	"npm/internal/types"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"go.uber.org/goleak"
)

// +------------+
// | Setup      |
// +------------+

type testsuite struct {
	suite.Suite
	mock          sqlmock.Sqlmock
	singleRow     *sqlmock.Rows
	listCountRows *sqlmock.Rows
	listRows      *sqlmock.Rows
}

// SetupTest is executed before each test
func (s *testsuite) SetupTest() {
	var err error
	s.mock, err = test.Setup()
	require.NoError(s.T(), err)

	// These rows need to be intantiated for each test as they are
	// read in the db object, and their row position is not resettable
	// between tests.
	s.singleRow = sqlmock.NewRows([]string{
		"id",
		"user_id",
		"name",
		"acmesh_name",
		"dns_sleep",
		"meta",
	}).AddRow(
		10,
		100,
		"Route53",
		"dns_aws",
		10,
		getMeta().Encoded,
	)

	s.listCountRows = sqlmock.NewRows([]string{
		"count(*)",
	}).AddRow(
		2,
	)

	s.listRows = sqlmock.NewRows([]string{
		"id",
		"user_id",
		"name",
		"acmesh_name",
		"dns_sleep",
		"meta",
	}).AddRow(
		10,
		100,
		"Route53",
		"dns_aws",
		10,
		getMeta().Encoded,
	).AddRow(
		11,
		100,
		"ClouDNS",
		"dns_cloudns",
		8,
		types.JSONB{},
	)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
	suite.Run(t, new(testsuite))
}

func getMeta() types.JSONB {
	m := types.JSONB{}
	m.UnmarshalJSON([]byte(`{"access_key_id": "BKINOTLNEREALYBL52W2I", "access_key": "NOTAREALKEY+9qSca7R9U6vUuetR8sh"}`))
	return m
}

func assertModel(t *testing.T, m Model) {
	assert.Equal(t, uint(10), m.ID, "ID not expected value")
	assert.Equal(t, uint(100), m.UserID, "UserID not expected value")
	assert.Equal(t, "Route53", m.Name, "Name not expected value")
	assert.Equal(t, "dns_aws", m.AcmeshName, "AcmeshName not expected value")
	assert.Equal(t, 10, m.DNSSleep, "DNSSleep not expected value")
	assert.Equal(t, getMeta(), m.Meta, "Meta not expected value")
}

// +------------+
// | Tests      |
// +------------+

func (s *testsuite) TestGetByID() {
	// goleak is used to detect goroutine leaks
	defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))

	s.mock.
		ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE "dns_provider"."id" = $1 AND "dns_provider"."is_deleted" = $2 ORDER BY "dns_provider"."id" LIMIT $3`)).
		WithArgs(10, 0, 1).
		WillReturnRows(s.singleRow)

	m, err := GetByID(10)
	require.NoError(s.T(), err)
	require.NoError(s.T(), s.mock.ExpectationsWereMet())
	assertModel(s.T(), m)
}

func (s *testsuite) TestGetAcmeShEnvVars() {
	// goleak is used to detect goroutine leaks
	defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))

	type want struct {
		envs []string
		err  error
	}

	tests := []struct {
		name        string
		dnsProvider Model
		metaJSON    string
		want        want
	}{
		{
			name: "dns_aws",
			dnsProvider: Model{
				AcmeshName: "dns_aws",
			},
			metaJSON: `{"AWS_ACCESS_KEY_ID":"sdfsdfsdfljlbjkljlkjsdfoiwje","AWS_SECRET_ACCESS_KEY":"xxxxxxx"}`,
			want: want{
				envs: []string{
					`AWS_ACCESS_KEY_ID=sdfsdfsdfljlbjkljlkjsdfoiwje`,
					`AWS_SECRET_ACCESS_KEY=xxxxxxx`,
				},
				err: nil,
			},
		},
		{
			name: "dns_cf",
			dnsProvider: Model{
				AcmeshName: "dns_cf",
			},
			metaJSON: `{"CF_Key":"sdfsdfsdfljlbjkljlkjsdfoiwje","CF_Email":"me@example.com","CF_Token":"dkfjghdk","CF_Account_ID":"hgbdjfg","CF_Zone_ID":"ASDASD"}`,
			want: want{
				envs: []string{
					`CF_Token=dkfjghdk`,
					`CF_Account_ID=hgbdjfg`,
					`CF_Zone_ID=ASDASD`,
					`CF_Key=sdfsdfsdfljlbjkljlkjsdfoiwje`,
					`CF_Email=me@example.com`,
				},
				err: nil,
			},
		},
		{
			name: "dns_duckdns",
			dnsProvider: Model{
				AcmeshName: "dns_duckdns",
			},
			metaJSON: `{"DuckDNS_Token":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}`,
			want: want{
				envs: []string{
					`DuckDNS_Token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`,
				},
				err: nil,
			},
		},
	}

	for _, tt := range tests {
		s.T().Run(tt.name, func(t *testing.T) {
			var meta types.JSONB
			err := json.Unmarshal([]byte(tt.metaJSON), &meta.Decoded)
			assert.Equal(t, nil, err)
			tt.dnsProvider.Meta = meta
			envs, err := tt.dnsProvider.GetAcmeShEnvVars()
			assert.Equal(t, tt.want.err, err)
			for _, i := range tt.want.envs {
				assert.Contains(t, envs, i)
			}
		})
	}
}

func (s *testsuite) TestList() {
	// goleak is used to detect goroutine leaks
	defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))

	s.mock.
		ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2`)).
		WithArgs("dns%", 0).
		WillReturnRows(s.listCountRows)

	s.mock.
		ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)).
		WithArgs("dns%", 0, 8).
		WillReturnRows(s.listRows)

	p := model.PageInfo{
		Offset: 0,
		Limit:  8,
		Sort: []model.Sort{
			{
				Field:     "name",
				Direction: "asc",
			},
		},
	}

	f := []model.Filter{
		{
			Field:    "acmesh_name",
			Modifier: "starts",
			Value:    []string{"dns"},
		},
	}

	resp, err := List(p, f)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), int64(2), resp.Total)
	assert.Equal(s.T(), p.Offset, resp.Offset)
	assert.Equal(s.T(), p.Limit, resp.Limit)
	assert.Equal(s.T(), p.Limit, resp.Limit)
	assert.Equal(s.T(), p.Sort, resp.Sort)
	assert.Equal(s.T(), f, resp.Filter)

	require.NoError(s.T(), s.mock.ExpectationsWereMet())
}

func (s *testsuite) TestSave() {
	// goleak is used to detect goroutine leaks
	defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))

	s.mock.ExpectBegin()
	s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "dns_provider" ("created_at","updated_at","is_deleted","user_id","name","acmesh_name","dns_sleep","meta") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)).
		WithArgs(
			sqlmock.AnyArg(),
			sqlmock.AnyArg(),
			0,
			100,
			"Route53",
			"dns_route53",
			10,
			sqlmock.AnyArg(),
		).
		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))
	s.mock.ExpectCommit()

	// New model, no user
	m := Model{
		Name:       "Route53",
		AcmeshName: "dns_route53",
		DNSSleep:   10,
		Meta:       getMeta(),
	}
	err := m.Save()
	assert.Equal(s.T(), "User ID must be specified", err.Error())

	// Success
	m.UserID = 100
	err = m.Save()
	require.NoError(s.T(), err)
	require.NoError(s.T(), s.mock.ExpectationsWereMet())
}

func (s *testsuite) TestDelete() {
	// goleak is used to detect goroutine leaks
	defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))

	s.mock.ExpectBegin()
	s.mock.
		ExpectExec(regexp.QuoteMeta(`UPDATE "dns_provider" SET "is_deleted"=$1 WHERE "dns_provider"."id" = $2 AND "dns_provider"."is_deleted" = $3`)).
		WithArgs(1, 10, 0).
		WillReturnResult(sqlmock.NewResult(0, 1))
	s.mock.ExpectCommit()

	m := Model{}
	err := m.Delete()
	assert.Equal(s.T(), "Unable to delete a new object", err.Error())

	m2 := Model{
		Base: model.Base{
			ID: 10,
		},
	}
	err2 := m2.Delete()
	require.NoError(s.T(), err2)
	require.NoError(s.T(), s.mock.ExpectationsWereMet())
}
