package testcontainers

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/scroll-tech/go-ethereum/ethclient"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"
	"gorm.io/gorm"

	"scroll-tech/common/database"
)

// TestcontainerApps testcontainers struct
type TestcontainerApps struct {
	postgresContainer *postgres.PostgresContainer
	l1GethContainer   *testcontainers.DockerContainer
	l2GethContainer   *testcontainers.DockerContainer

	// common time stamp in nanoseconds.
	Timestamp int
}

// NewTestcontainerApps returns new instance of TestcontainerApps struct
func NewTestcontainerApps() *TestcontainerApps {
	timestamp := time.Now().Nanosecond()
	return &TestcontainerApps{
		Timestamp: timestamp,
	}
}

// StartPostgresContainer starts a postgres container
func (t *TestcontainerApps) StartPostgresContainer() error {
	if t.postgresContainer != nil && t.postgresContainer.IsRunning() {
		return nil
	}
	postgresContainer, err := postgres.RunContainer(context.Background(),
		testcontainers.WithImage("postgres"),
		postgres.WithDatabase("test_db"),
		postgres.WithPassword("123456"),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").WithOccurrence(2).WithStartupTimeout(5*time.Second)),
	)
	if err != nil {
		log.Printf("failed to start postgres container: %s", err)
		return err
	}
	t.postgresContainer = postgresContainer
	return nil
}

// StartL1GethContainer starts a L1Geth container
func (t *TestcontainerApps) StartL1GethContainer() error {
	if t.l1GethContainer != nil && t.l1GethContainer.IsRunning() {
		return nil
	}
	req := testcontainers.ContainerRequest{
		Image:        "scroll_l1geth",
		ExposedPorts: []string{"8546/tcp", "8545/tcp"},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort("8546").WithStartupTimeout(100*time.Second),
			wait.ForListeningPort("8545").WithStartupTimeout(100*time.Second),
		),
		Cmd: []string{"--log.debug", "ANY"},
	}
	genericContainerReq := testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	}
	container, err := testcontainers.GenericContainer(context.Background(), genericContainerReq)
	if err != nil {
		log.Printf("failed to start scroll_l1geth container: %s", err)
		return err
	}
	t.l1GethContainer, _ = container.(*testcontainers.DockerContainer)
	return nil
}

// StartL2GethContainer starts a L2Geth container
func (t *TestcontainerApps) StartL2GethContainer() error {
	if t.l2GethContainer != nil && t.l2GethContainer.IsRunning() {
		return nil
	}
	req := testcontainers.ContainerRequest{
		Image:        "scroll_l2geth",
		ExposedPorts: []string{"8546/tcp", "8545/tcp"},
		WaitingFor: wait.ForAll(
			wait.ForListeningPort("8546").WithStartupTimeout(100*time.Second),
			wait.ForListeningPort("8545").WithStartupTimeout(100*time.Second),
		),
	}
	genericContainerReq := testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	}
	container, err := testcontainers.GenericContainer(context.Background(), genericContainerReq)
	if err != nil {
		log.Printf("failed to start scroll_l2geth container: %s", err)
		return err
	}
	t.l2GethContainer, _ = container.(*testcontainers.DockerContainer)
	return nil
}

// GetDBEndPoint returns the endpoint of the running postgres container
func (t *TestcontainerApps) GetDBEndPoint() (string, error) {
	if t.postgresContainer == nil || !t.postgresContainer.IsRunning() {
		return "", fmt.Errorf("postgres is not running")
	}
	return t.postgresContainer.ConnectionString(context.Background(), "sslmode=disable")
}

// GetL1GethEndPoint returns the endpoint of the running L1Geth container
func (t *TestcontainerApps) GetL1GethEndPoint() (string, error) {
	if t.l1GethContainer == nil || !t.l1GethContainer.IsRunning() {
		return "", fmt.Errorf("l1 geth is not running")
	}
	endpoint, err := t.l1GethContainer.PortEndpoint(context.Background(), "8546/tcp", "ws")
	if err != nil {
		return "", err
	}
	return endpoint, nil
}

// GetL2GethEndPoint returns the endpoint of the running L2Geth container
func (t *TestcontainerApps) GetL2GethEndPoint() (string, error) {
	if t.l2GethContainer == nil || !t.l2GethContainer.IsRunning() {
		return "", fmt.Errorf("l2 geth is not running")
	}
	endpoint, err := t.l2GethContainer.PortEndpoint(context.Background(), "8546/tcp", "ws")
	if err != nil {
		return "", err
	}
	return endpoint, nil
}

// GetGormDBClient returns a gorm.DB by connecting to the running postgres container
func (t *TestcontainerApps) GetGormDBClient() (*gorm.DB, error) {
	endpoint, err := t.GetDBEndPoint()
	if err != nil {
		return nil, err
	}
	dbCfg := &database.Config{
		DSN:        endpoint,
		DriverName: "postgres",
		MaxOpenNum: 200,
		MaxIdleNum: 20,
	}
	return database.InitDB(dbCfg)
}

// GetL1GethClient returns a ethclient by dialing running L1Geth
func (t *TestcontainerApps) GetL1GethClient() (*ethclient.Client, error) {
	endpoint, err := t.GetL1GethEndPoint()
	if err != nil {
		return nil, err
	}
	client, err := ethclient.Dial(endpoint)
	if err != nil {
		return nil, err
	}
	return client, nil
}

// GetL2GethClient returns a ethclient by dialing running L2Geth
func (t *TestcontainerApps) GetL2GethClient() (*ethclient.Client, error) {
	endpoint, err := t.GetL2GethEndPoint()
	if err != nil {
		return nil, err
	}
	client, err := ethclient.Dial(endpoint)
	if err != nil {
		return nil, err
	}
	return client, nil
}

// Free stops all running containers
func (t *TestcontainerApps) Free() {
	ctx := context.Background()
	if t.postgresContainer != nil && t.postgresContainer.IsRunning() {
		if err := t.postgresContainer.Terminate(ctx); err != nil {
			log.Printf("failed to stop postgres container: %s", err)
		}
	}
	if t.l1GethContainer != nil && t.l1GethContainer.IsRunning() {
		if err := t.l1GethContainer.Terminate(ctx); err != nil {
			log.Printf("failed to stop scroll_l1geth container: %s", err)
		}
	}
	if t.l2GethContainer != nil && t.l2GethContainer.IsRunning() {
		if err := t.l2GethContainer.Terminate(ctx); err != nil {
			log.Printf("failed to stop scroll_l2geth container: %s", err)
		}
	}
}
