//go:build ignore

package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sync"
	"time"

	"google.golang.org/protobuf/proto"
	"nhooyr.io/websocket"

	toki "git.toki-labs.com/toki/proto-socket/go"
	"git.toki-labs.com/toki/proto-socket/go/packets"
)

const (
	host                    = "127.0.0.1"
	goDartWebWSPort         = 29198
	goDartWebWSSPort        = 29199
	wsPath                  = "/"
	processTimeout          = 60 * time.Second
	serverObservationWindow = 200 * time.Millisecond
)

func parserMap() toki.ParserMap {
	return toki.ParserMap{
		toki.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
			m := &packets.TestData{}
			return m, proto.Unmarshal(b, m)
		},
	}
}

func browserAcceptOptions() *websocket.AcceptOptions {
	return &websocket.AcceptOptions{
		OriginPatterns: []string{"localhost:*", "127.0.0.1:*"},
	}
}

func main() {
	fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "FAIL crosstest error=%v\n", err)
		os.Exit(1)
	}
	fmt.Println("PASS scenario=ws-send-push")
	fmt.Println("PASS scenario=ws-request-response")
	fmt.Println("PASS scenario=wss-send-push")
	fmt.Println("PASS scenario=wss-request-response")
	fmt.Println("PASS all go-server/dart-web-client crosstests passed")
}

func run() error {
	serverTLS, err := loadWebServerTLS()
	if err != nil {
		return err
	}
	return runWebSuites(serverTLS)
}

func loadWebServerTLS() (*tls.Config, error) {
	dartDir, err := dartPackageDir()
	if err != nil {
		return nil, err
	}
	certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
	keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, err
	}
	return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}

func runWebSuites(serverTLS *tls.Config) error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	wsReceived := make(chan bool, 1)
	wssReceived := make(chan bool, 1)
	wsServer := newWebServer(goDartWebWSPort, nil, wsReceived)
	wssServer := newWebServer(goDartWebWSSPort, serverTLS, wssReceived)

	if err := wsServer.Start(ctx); err != nil {
		return err
	}
	defer wsServer.Stop()
	if err := wssServer.Start(ctx); err != nil {
		return err
	}
	defer wssServer.Stop()

	if err := runDartBrowserTest(); err != nil {
		return err
	}
	if err := expectReceived("WS", wsReceived); err != nil {
		return err
	}
	return expectReceived("WSS", wssReceived)
}

func newWebServer(port int, tlsCfg *tls.Config, received chan<- bool) *toki.WsServer {
	options := toki.WsServerOptions{
		AcceptOptions: browserAcceptOptions(),
	}
	newClient := func(conn *websocket.Conn) *toki.WsClient {
		return toki.NewWsClient(conn, 0, 0, parserMap())
	}
	var server *toki.WsServer
	if tlsCfg != nil {
		server = toki.NewWsServerTLSWithOptions(host, port, wsPath, tlsCfg, options, newClient)
	} else {
		server = toki.NewWsServerWithOptions(host, port, wsPath, options, newClient)
	}
	server.OnClientConnected = func(client *toki.WsClient) {
		toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
			fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", req.GetIndex(), req.GetMessage())
			if req.GetIndex() == 101 && req.GetMessage() == "fire from dart web client" {
				select {
				case received <- true:
				default:
				}
				_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
			}
			return &packets.TestData{
				Index:   req.GetIndex() * 2,
				Message: "echo: " + req.GetMessage(),
			}, nil
		})
	}
	return server
}

func expectReceived(transport string, received <-chan bool) error {
	select {
	case ok := <-received:
		if !ok {
			return fmt.Errorf("%s web send-push server received unexpected data", transport)
		}
	case <-time.After(serverObservationWindow):
		return fmt.Errorf("%s web send-push server did not receive expected data", transport)
	}
	return nil
}

func runDartBrowserTest() error {
	ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
	defer cancel()

	dartDir, err := dartPackageDir()
	if err != nil {
		return err
	}

	cmd := exec.CommandContext(ctx, "dart", "test", "-p", "chrome", "test/browser_ws_runtime_test.dart")
	cmd.Dir = dartDir
	cmd.WaitDelay = 5 * time.Second

	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	stderrPipe, err := cmd.StderrPipe()
	if err != nil {
		return err
	}
	if err := cmd.Start(); err != nil {
		return err
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go pipeLines(&wg, stdoutPipe, os.Stdout)
	go pipeLines(&wg, stderrPipe, os.Stderr)

	waitErr := cmd.Wait()
	done := make(chan struct{})
	go func() { wg.Wait(); close(done) }()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		return fmt.Errorf("dart browser test pipe scan timed out")
	}
	if ctx.Err() == context.DeadlineExceeded {
		return fmt.Errorf("dart browser test timed out")
	}
	if waitErr != nil {
		return fmt.Errorf("dart browser test failed: %w", waitErr)
	}
	return nil
}

func pipeLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File) {
	defer wg.Done()
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		fmt.Fprintln(writer, scanner.Text())
	}
}

func dartPackageDir() (string, error) {
	candidates := make([]string, 0, 3)

	_, filename, _, ok := runtime.Caller(0)
	if ok {
		repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
		candidates = append(candidates, filepath.Join(repoRoot, "dart"))
	}
	if wd, err := os.Getwd(); err == nil {
		candidates = append(candidates, findDartPackageCandidates(wd)...)
	}
	if executable, err := os.Executable(); err == nil {
		candidates = append(candidates, findDartPackageCandidates(filepath.Dir(executable))...)
	}

	for _, candidate := range candidates {
		if isDartPackageDir(candidate) {
			return candidate, nil
		}
	}
	return "", fmt.Errorf("cannot resolve dart package directory from candidates %v", candidates)
}

func findDartPackageCandidates(start string) []string {
	candidates := make([]string, 0)
	for dir := start; ; dir = filepath.Dir(dir) {
		candidates = append(candidates, filepath.Join(dir, "dart"))
		if filepath.Base(dir) == "dart" {
			candidates = append(candidates, dir)
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return candidates
		}
	}
}

func isDartPackageDir(dir string) bool {
	info, err := os.Stat(filepath.Join(dir, "pubspec.yaml"))
	return err == nil && !info.IsDir()
}
