package setup

import (
	"context"
	"log"
	"strconv"

	redisadapter "git.gocasts.ir/ebhomengo/niki/adapter/redis"
	"git.gocasts.ir/ebhomengo/niki/repository/mysql"
	// nolint
	_ "github.com/go-sql-driver/mysql"
	"github.com/ory/dockertest/v3"
	"github.com/ory/dockertest/v3/docker"
)

// TestContainer holds the state of docker test.
type TestContainer struct {
	dockerPool               *dockertest.Pool      // the connection pool to Docker.
	mariaResource            *dockertest.Resource  // MariaDB Docker container resource.
	redisResource            *dockertest.Resource  // Redis Docker container resource.
	mariaDBConn              *mysql.DB             // Connection to the MariaDB mysql.
	redisDBConn              *redisadapter.Adapter // Connection to the Redis mysql.
	containerExpiryInSeconds uint
}

func NewTestContainer() *TestContainer {
	return &TestContainer{
		// nolint
		containerExpiryInSeconds: 120,
	}
}

func (t *TestContainer) GetMariaDBConnection() *mysql.DB {
	if t.mariaDBConn == nil {
		log.Fatal("did you forgot to start the test container?")
	}

	return t.mariaDBConn
}

func (t *TestContainer) GetRedisDBConnection() *redisadapter.Adapter {
	if t.redisDBConn == nil {
		log.Fatal("did you forgot to start the test container?")
	}

	return t.redisDBConn
}

func (t *TestContainer) GetMariaDBConfig() mysql.Config {
	if t.mariaResource == nil {
		log.Fatal("did you forgot to start the test container?")
	}

	port, err := strconv.Atoi(t.mariaResource.GetPort("3306/tcp"))
	if err != nil {
		log.Fatalf("couldn't convert port to integer: %s", err)
	}

	return mysql.Config{
		Username: "root",
		Password: "secret",
		Port:     port,
		Host:     "localhost",
		DBName:   "mysql",
	}
}

func (t *TestContainer) GetRedisConfig() redisadapter.Config {
	if t.mariaResource == nil {
		log.Fatal("did you forgot to start the test container?")
	}

	port, err := strconv.Atoi(t.redisResource.GetPort("6379/tcp"))
	if err != nil {
		log.Fatalf("couldn't convert port to integer: %s", err)
	}

	return redisadapter.Config{
		Host:     "localhost",
		Port:     port,
		Password: "",
		DB:       0,
	}
}

// Start initializes and starts the MariaDB and Redis Docker container for testing.
func (t *TestContainer) Start() {
	// uses a sensible default on windows (tcp/http) and linux/osx (socket)
	var err error
	t.dockerPool, err = dockertest.NewPool("")
	if err != nil {
		log.Fatalf("Could not construct pool: %s", err)
	}

	// uses pool to try to connect to Docker
	err = t.dockerPool.Client.Ping()
	if err != nil {
		log.Fatalf("Could not connect to Docker: %s", err)
	}

	// pulls mariaDB image, creates a container based on it and runs it
	t.mariaResource, err = t.dockerPool.RunWithOptions(
		&dockertest.RunOptions{
			Repository: "bitnami/mariadb",
			Tag:        "11.1",
			Env:        []string{"MARIADB_ROOT_PASSWORD=secret"},
		}, func(config *docker.HostConfig) {
			config.AutoRemove = true // Ensure the container is removed after the test.
			config.RestartPolicy = docker.RestartPolicy{
				Name: "no", // Do not automatically restart the container.
			}
		},
	)
	if err != nil {
		log.Fatalf("Could not start maria resource: %s", err)
	}

	// Set container to expire after two minutes to avoid dangling resources in case of test interruption.
	if err = t.mariaResource.Expire(t.containerExpiryInSeconds); err != nil {
		log.Fatalf("Couldn't set MariaDB container expiration: %s", err)
	}

	t.redisResource, err = t.dockerPool.RunWithOptions(
		&dockertest.RunOptions{
			Repository: "bitnami/redis",
			Tag:        "6.2",
			Env:        []string{"ALLOW_EMPTY_PASSWORD=yes"},
		}, func(config *docker.HostConfig) {
			config.AutoRemove = true // Ensure the container is removed after the test.
			config.RestartPolicy = docker.RestartPolicy{
				Name: "no", // Do not automatically restart the container.
			}
		},
	)
	if err != nil {
		log.Fatalf("Could not start redis resource: %s", err)
	}

	// Set container to expire after two minutes to avoid dangling resources in case of test interruption.
	if err = t.redisResource.Expire(t.containerExpiryInSeconds); err != nil {
		log.Fatalf("Couldn't set RedisDB container expiration: %s", err)
	}

	// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
	if err = t.dockerPool.Retry(func() error {
		var err error
		t.mariaDBConn = mysql.New(t.GetMariaDBConfig())
		err = t.mariaDBConn.Conn().Ping()
		if err != nil {
			return err
		}

		t.redisDBConn = redisadapter.New(t.GetRedisConfig())
		err = t.redisDBConn.Client().Ping(context.Background()).Err()
		if err != nil {
			return err
		}

		return nil
	}); err != nil {
		log.Fatalf("Could not connect to mysql: %s", err)
	}
}

// Stop cleans up the Docker containers.
func (t *TestContainer) Stop() {
	err := t.mariaDBConn.CloseStatements()
	if err != nil {
		log.Fatalf("Could not close statements on maria db connection: %s", err)
	}
	err = t.mariaDBConn.Conn().Close()
	if err != nil {
		log.Fatalf("Could not close maria db connection: %s", err)
	}

	if err = t.dockerPool.Purge(t.mariaResource); err != nil {
		log.Fatalf("Could not purge maria resource: %s", err)
	}

	if err = t.dockerPool.Purge(t.redisResource); err != nil {
		log.Fatalf("Could not purge redis resource: %s", err)
	}
}
