//go:build integration

package app_test

import (
	"context"
	"fmt"
	"runtime"
	"sync"
	"testing"
	"time"

	"git.netflux.io/rob/octoplex/internal/app"
	"git.netflux.io/rob/octoplex/internal/config"
	"git.netflux.io/rob/octoplex/internal/domain"
	"git.netflux.io/rob/octoplex/internal/terminal"
	"git.netflux.io/rob/octoplex/internal/testhelpers"
	dockerclient "github.com/docker/docker/client"
	"github.com/gdamore/tcell/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

func TestIntegration(t *testing.T) {
	ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
	defer cancel()

	destServer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image:        "bluenviron/mediamtx:latest",
			Env:          map[string]string{"MTX_RTMPADDRESS": ":1936"},
			ExposedPorts: []string{"1936/tcp"},
			WaitingFor:   wait.ForListeningPort("1936/tcp"),
		},
		Started: true,
	})
	testcontainers.CleanupContainer(t, destServer)
	require.NoError(t, err)
	destServerPort, err := destServer.MappedPort(ctx, "1936/tcp")
	require.NoError(t, err)

	logger := testhelpers.NewTestLogger().With("component", "integration")
	dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
	require.NoError(t, err)

	// Fetching the screen contents is tricky at this level of the test pyramid,
	// because we need to:
	//
	// 1. Somehow capture the screen contents, which is only available via the
	//    tcell.SimulationScreen, and...
	// 2. Do so without triggering data races.
	//
	// We can achieve this by passing a channel into the terminal actor, which
	// will send screen captures after each render. This can be stored locally
	// and asserted against when needed.
	var (
		screenCells []tcell.SimCell
		screenWidth int
		screenMu    sync.Mutex
	)

	getContents := func() []string {
		screenMu.Lock()
		defer screenMu.Unlock()

		var lines []string
		for n, _ := range screenCells {
			y := n / screenWidth

			if y > len(lines)-1 {
				lines = append(lines, "")
			}
			lines[y] += string(screenCells[n].Runes[0])
		}

		return lines
	}

	screen := tcell.NewSimulationScreen("")
	screenCaptureC := make(chan terminal.ScreenCapture, 1)
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case capture := <-screenCaptureC:
				screenMu.Lock()
				screenCells = capture.Cells
				screenWidth = capture.Width
				screenMu.Unlock()
			}
		}
	}()

	done := make(chan struct{})
	go func() {
		// https://stackoverflow.com/a/60740997/62871
		if runtime.GOOS != "linux" {
			panic("TODO: try host.docker.internal or Mac equivalent here")
		}
		const destHost = "172.17.0.1"

		err := app.Run(ctx, app.RunParams{
			Config: config.Config{
				Sources: config.Sources{
					RTMP: config.RTMPSource{
						Enabled:   true,
						StreamKey: "live",
					},
				},
				Destinations: []config.Destination{
					{
						Name: "Local server 1",
						URL:  fmt.Sprintf("rtmp://%s:%d/live/dest1", destHost, destServerPort.Int()),
					},
					{
						Name: "Local server 2",
						URL:  fmt.Sprintf("rtmp://%s:%d/live/dest2", destHost, destServerPort.Int()),
					},
				},
			},
			DockerClient: dockerClient,
			Screen: &terminal.Screen{
				Screen:   screen,
				Width:    160,
				Height:   25,
				CaptureC: screenCaptureC,
			},
			ClipboardAvailable: false,
			BuildInfo:          domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
			Logger:             logger,
		})
		require.NoError(t, err)

		done <- struct{}{}
	}()

	// Wait for mediaserver container to start:
	time.Sleep(5 * time.Second)

	// Start streaming a test video to the app:
	testhelpers.StreamFLV(t, "rtmp://localhost:1935/live")
	time.Sleep(10 * time.Second)

	// Start destinations:
	screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
	screen.PostEvent(tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone))
	screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))

	require.EventuallyWithT(
		t,
		func(t *assert.CollectT) {
			contents := getContents()
			require.True(t, len(contents) > 2, "expected at least 3 lines of output")

			assert.Contains(t, contents[2], "Status   receiving", "expected mediaserver status to be receiving")
			assert.Contains(t, contents[3], "Tracks   H264", "expected mediaserver tracks to be H264")
			assert.Contains(t, contents[4], "Health   healthy", "expected mediaserver to be healthy")

			require.Contains(t, contents[2], "Local server 1", "expected local server 1 to be present")
			assert.Contains(t, contents[2], "sending", "expected local server 1 to be sending")
			assert.Contains(t, contents[2], "healthy", "expected local server 1 to be healthy")

			require.Contains(t, contents[3], "Local server 2", "expected local server 2 to be present")
			assert.Contains(t, contents[3], "sending", "expected local server 2 to be sending")
			assert.Contains(t, contents[3], "healthy", "expected local server 2 to be healthy")

		},
		2*time.Minute,
		time.Second,
	)

	// Stop destinations:
	screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
	screen.PostEvent(tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone))
	screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))

	time.Sleep(10 * time.Second)

	require.EventuallyWithT(
		t,
		func(t *assert.CollectT) {
			contents := getContents()
			require.True(t, len(contents) > 2, "expected at least 3 lines of output")

			require.Contains(t, contents[2], "Local server 1", "expected local server 1 to be present")
			assert.Contains(t, contents[2], "exited", "expected local server 1 to have exited")

			require.Contains(t, contents[3], "Local server 2", "expected local server 2 to be present")
			assert.Contains(t, contents[3], "exited", "expected local server 2 to have exited")
		},
		2*time.Minute,
		time.Second,
	)

	// TODO:
	// - Source error
	// - Destination error
	// - Additional features (copy URL, etc.)

	cancel()

	<-done
}