package integrationtest

import (
	"context"
	"fmt"
	"git.wamblee.org/converge/pkg/support/ioutils"
	"git.wamblee.org/converge/pkg/support/sshsupport"
	"git.wamblee.org/converge/pkg/testsupport"
	"github.com/stretchr/testify/suite"
	testcontainers "github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/network"
	"github.com/testcontainers/testcontainers-go/wait"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

const (
	agentEntryPointTemplate = ` 
        curl -v http://converge:8000/downloads/agent > agent
	    chmod 755 agent 
	    echo "{{.publicKey}}" > .authorized_keys
	    cat .authorized_keys
        echo "HOSTNAME=$( hostname )"
	    ./agent --id abc ws://converge:8000
	    tail -f /dev/null `

	clientEntryPointTemplate = `
        echo "HOSTNAME_OF_AGENT_ON_CLIENT=agent"
        echo Ready 
        sleep 1000000
    `
	clientEntryPointTemplatex = `
        curl -v http://converge:8000/downloads/wsproxy > wsproxy
        chmod 755 wsproxy
        echo "{{.privateKey}}" > id_rsa
        chmod 400 id_rsa
        echo "===================================================================================================="
        echo "
          hostname | sed 's/^/===============  HOSTNAME_OF_AGENT_ON_CLIENT=/g' " | 
        ssh -tt -oStrictHostKeyChecking=no -i id_rsa -oProxyCommand="./wsproxy ws://converge:8000/client/abc" localhost
        echo "===================================================================================================="
        echo Ready
        sleep 1000000`
)

var (
	version       = getTag()
	convergeImage = fmt.Sprintf("%s/converge:%s", getRegistry(), version)
	agentImage    = fmt.Sprintf("%s/converge-test:%s", getRegistry(), version)
	clientImage   = agentImage
)

func getTag() string {
	tag, ok := os.LookupEnv("IMAGE_TAG")
	if ok {
		return tag
	}
	return "1.0.0"
}

func getRegistry() string {
	registry, ok := os.LookupEnv("REGISTRY")
	if ok {
		return registry
	}
	moduleDir := testsupport.GetGoModDIr()
	log.Printf("Go module directory %s", moduleDir)
	envFile := filepath.Join(moduleDir, ".env")
	file, err := os.Open(envFile)
	if err != nil {
		panic("Could not find .env file and REGISTRY env var is not set")
	}
	defer file.Close()
	props, err := ioutils.ReadPropertyFile(file)
	if err != nil {
		panic(fmt.Sprintf("Error reading environment file '%v': %v", envFile, err))
	}
	registry, ok = props["REGISTRY"]
	if !ok {
		panic(fmt.Sprintf("Could not find REGISTRY setting in property file '%v'", envFile))
	}
	return registry
}

type IntegrationTestSuite struct {
	suite.Suite

	ctx        context.Context
	cancelFunc context.CancelFunc
}

func Test_IntegrationTestSuite(t *testing.T) {
	suite.Run(t, &IntegrationTestSuite{})
}

func (s *IntegrationTestSuite) SetupSuite() {
}

func (s *IntegrationTestSuite) TearDownSuite() {
}

func (s *IntegrationTestSuite) SetupTest() {
	ctx, cancelFunc := testsupport.CreateTestContext(context.Background(), 60*time.Second)
	s.ctx = ctx
	s.cancelFunc = cancelFunc
}

func (s *IntegrationTestSuite) TearDownTest() {
	s.cancelFunc()
}

type ContainerLogger func(logtype string, msg string)

func logContainerOutput(container string, logtype string, msg string) {
	log.Printf("%s: %s: %s", container, logtype, msg)
}

func containerLogger(container string) ContainerLogger {
	return func(logtype string, msg string) {
		logContainerOutput(container, logtype, msg)
	}
}

func (c ContainerLogger) Accept(log testcontainers.Log) {
	c(log.LogType, string(log.Content))
}

func createLogConsumerConfig(container string) *testcontainers.LogConsumerConfig {
	return &testcontainers.LogConsumerConfig{
		Opts:      []testcontainers.LogProductionOption{testcontainers.WithLogProductionTimeout(10 * time.Second)},
		Consumers: []testcontainers.LogConsumer{containerLogger(container)},
	}
}

func (s *IntegrationTestSuite) runCmd(container testcontainers.Container, cmd []string) (int, string, error) {
	status, reader, err := container.Exec(s.ctx, cmd)
	if err != nil {
		return 0, "", err
	}
	output, err := io.ReadAll(reader)
	if err != nil {
		return 0, "", err
	}
	return status, string(output), nil
}

func (s *IntegrationTestSuite) runCmdFailOnError(container testcontainers.Container, cmd []string) (int, string) {
	status, output, err := s.runCmd(container, cmd)
	s.Require().Nil(err)
	return status, output
}

func (s *IntegrationTestSuite) defineContainer(net *testcontainers.DockerNetwork, hostname string,
	image string,
	logLine string,
	ports []string) testcontainers.GenericContainerRequest {

	log.Printf("PORTS %v", ports)

	waitStrategies := []wait.Strategy{wait.ForLog(logLine)}
	if len(ports) > 0 {
		waitStrategies = append(waitStrategies, wait.ForExposedPort())
	}

	request := testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image:          image,
			ExposedPorts:   ports,
			Hostname:       hostname,
			WaitingFor:     wait.ForAll(waitStrategies...).WithStartupTimeoutDefault(30 * time.Second),
			LogConsumerCfg: createLogConsumerConfig(hostname),
			Networks:       []string{net.Name},
			//NetworkAliases: map[string][]string{
			//	net.Name: []string{hostname},
			//},
		},
		Started: false,
	}
	return request
}

func (s *IntegrationTestSuite) Test_SingleAgentAndClient() {

	net, err := network.New(s.ctx)
	s.Require().Nil(err)
	defer net.Remove(s.ctx)

	// SSH key to use
	privateKey, publicKey, err := sshsupport.GeneratePrivatePublicKey(2048)
	s.Require().Nil(err)

	// The server

	log.Println("Starting converge server")
	converge, err := testcontainers.GenericContainer(s.ctx,
		s.defineContainer(net, "converge",
			convergeImage,
			"Rendez-vous server listening",
			[]string{"8000/tcp"}))
	s.Require().Nil(err)
	err = converge.Start(s.ctx)
	s.Require().Nil(err)
	defer converge.Terminate(s.ctx)

	port, err := converge.MappedPort(s.ctx, "8000/tcp")
	s.Require().Nil(err)
	log.Printf("External port %v", port)

	// The agent
	log.Println("Starting agent")
	agentEntrypoint := testsupport.Template(agentEntryPointTemplate,
		map[string]string{"publicKey": publicKey})

	agentRequest := s.defineContainer(net, "agent", agentImage,
		"starting ssh server, waiting for debug sessions", []string{})
	agentRequest.Entrypoint = []string{
		"sh", "-c", agentEntrypoint}
	agent, err := testcontainers.GenericContainer(s.ctx, agentRequest)
	s.Require().Nil(err)
	err = agent.Start(s.ctx)
	log.Printf("Error from starting agent %+v", err)
	s.Require().Nil(err)
	defer agent.Terminate(s.ctx)

	// The client

	log.Println("Starting client")
	clientEntrypoint := testsupport.Template(clientEntryPointTemplate,
		map[string]string{"privateKey": privateKey})
	clientRequest := s.defineContainer(net, "client", clientImage,
		"Ready", []string{})
	clientRequest.Entrypoint = []string{
		"sh", "-c", clientEntrypoint}
	client, err := testcontainers.GenericContainer(s.ctx, clientRequest)
	s.Require().Nil(err)
	err = client.Start(s.ctx)
	log.Printf("Error from starting client %+v", err)
	s.Require().Nil(err)
	defer client.Terminate(s.ctx)

	input, err := client.Logs(s.ctx)
	s.Require().Nil(err)
	inputBytes, err := io.ReadAll(input)
	s.Require().Nil(err)
	s.True(strings.Contains(string(inputBytes), "HOSTNAME_OF_AGENT_ON_CLIENT=agent"))
}