package tenants

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"io"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type dbFixture struct {
	queries []dbQuery
	query   func(string, []driver.NamedValue) (driver.Rows, error)
}

type dbQuery struct {
	query string
	args  []driver.NamedValue
}

type fixtureConnector struct{ fixture *dbFixture }

func (c fixtureConnector) Connect(context.Context) (driver.Conn, error) {
	return fixtureConn{fixture: c.fixture}, nil
}

func (c fixtureConnector) Driver() driver.Driver { return fixtureDriver{} }

type fixtureDriver struct{}

func (fixtureDriver) Open(string) (driver.Conn, error) {
	return nil, errors.New("fixture driver requires OpenDB")
}

type fixtureConn struct{ fixture *dbFixture }

func (fixtureConn) Prepare(string) (driver.Stmt, error) {
	return nil, errors.New("prepare is not supported")
}

func (fixtureConn) Close() error { return nil }

func (fixtureConn) Begin() (driver.Tx, error) {
	return nil, errors.New("transactions are not supported")
}

func (c fixtureConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	c.fixture.queries = append(c.fixture.queries, dbQuery{query: query, args: append([]driver.NamedValue(nil), args...)})
	return c.fixture.query(query, args)
}

type fixtureRows struct {
	columns []string
	rows    [][]driver.Value
	nextErr error
	index   int
}

func (r *fixtureRows) Columns() []string { return r.columns }

func (r *fixtureRows) Close() error { return nil }

func (r *fixtureRows) Next(dest []driver.Value) error {
	if r.index < len(r.rows) {
		copy(dest, r.rows[r.index])
		r.index++
		return nil
	}
	if r.nextErr != nil {
		err := r.nextErr
		r.nextErr = nil
		return err
	}
	return io.EOF
}

func newVMInfoQuerier(t *testing.T, fixture *dbFixture) *mysqlQuerier {
	t.Helper()
	db := sql.OpenDB(fixtureConnector{fixture: fixture})
	t.Cleanup(func() { require.NoError(t, db.Close()) })
	return &mysqlQuerier{db: db}
}

func vmInfoRows(rows ...[]driver.Value) *fixtureRows {
	return &fixtureRows{
		columns: []string{
			"application_id", "cluster_id", "deletion_timestamp",
			"update_timestamp", "vm_account_id", "vm_project_id",
		},
		rows: rows,
	}
}

func TestQueryVMInfoRowsAndDeduplication(t *testing.T) {
	older := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
	newer := older.Add(time.Hour)
	fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
		return vmInfoRows(
			[]driver.Value{"app-1", "cluster-old", nil, older, nil, nil},
			[]driver.Value{"app-1", "cluster-new", nil, newer, "10", "20"},
			[]driver.Value{"app-2", "cluster-2", older, newer, nil, nil},
		), nil
	}}

	got, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), []string{"app-1", "app-2"})
	require.NoError(t, err)
	require.Len(t, got, 2)
	assert.Equal(t, "cluster-new", got["app-1"].InternalClusterID)
	assert.Equal(t, "10", got["app-1"].VMAccountID.String)
	assert.True(t, got["app-2"].DeletionTime.Valid)
	assert.False(t, got["app-2"].VMAccountID.Valid)

	require.Len(t, fixture.queries, 1)
	require.Len(t, fixture.queries[0].args, 2)
	assert.Equal(t, 2, strings.Count(fixture.queries[0].query, "?"))
	assert.Equal(t, "app-1", fixture.queries[0].args[0].Value)
	assert.Equal(t, "app-2", fixture.queries[0].args[1].Value)
}

func TestQueryVMInfoBatchBoundary(t *testing.T) {
	fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
		return vmInfoRows(), nil
	}}
	appIDs := make([]string, essentialDBBatch+1)
	for i := range appIDs {
		appIDs[i] = string(rune('a' + i%26))
	}

	got, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), appIDs)
	require.NoError(t, err)
	assert.Empty(t, got)
	require.Len(t, fixture.queries, 2)
	assert.Len(t, fixture.queries[0].args, essentialDBBatch)
	assert.Len(t, fixture.queries[1].args, 1)
	assert.Equal(t, essentialDBBatch, strings.Count(fixture.queries[0].query, "?"))
	assert.Equal(t, 1, strings.Count(fixture.queries[1].query, "?"))
}

func TestQueryVMInfoEmptyInputSkipsDatabase(t *testing.T) {
	fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
		t.Fatal("database queried for empty input")
		return nil, nil
	}}

	got, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), nil)
	require.NoError(t, err)
	assert.Empty(t, got)
	assert.Empty(t, fixture.queries)
}

func TestQueryVMInfoErrors(t *testing.T) {
	t.Run("Query", func(t *testing.T) {
		fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
			return nil, errors.New("query failed")
		}}
		_, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), []string{"app-1"})
		assert.ErrorContains(t, err, "query application_ids batch")
	})

	t.Run("Scan", func(t *testing.T) {
		fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
			return vmInfoRows([]driver.Value{"app-1", make(chan int), nil, nil, nil, nil}), nil
		}}
		_, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), []string{"app-1"})
		assert.ErrorContains(t, err, "scan")
	})

	t.Run("Rows", func(t *testing.T) {
		fixture := &dbFixture{query: func(string, []driver.NamedValue) (driver.Rows, error) {
			rows := vmInfoRows()
			rows.nextErr = errors.New("rows failed")
			return rows, nil
		}}
		_, err := newVMInfoQuerier(t, fixture).QueryVMInfo(t.Context(), []string{"app-1"})
		assert.ErrorContains(t, err, "rows iter")
	})
}
