//go:build ignore

package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"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"
	goTypescriptTCPPort     = 29490
	goTypescriptWSPort      = 29492
	goTypescriptTLSPort     = 29494
	goTypescriptWSSPort     = 29496
	wsPath                  = "/"
	processTimeout          = 20 * 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 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 all go-server/typescript-client crosstests passed")
}

func run() error {
	selectedTransport := os.Getenv("PROTO_SOCKET_CROSSTEST_TRANSPORT")
	if selectedTransport == "" || selectedTransport == "tcp" {
		if err := runTCPSendPush(); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
		if err := runTCPRequests(); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
		if err := runTCPLegacySimpleReceive(); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
	}
	if selectedTransport == "" || selectedTransport == "ws" {
		if err := runWSSendPush(); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
		if err := runWSRequests(); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
	}

	dartDir, err := dartPackageDir()
	if err != nil {
		return fmt.Errorf("cannot find dart package dir: %w", err)
	}
	certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
	keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
	serverTLS, err := loadServerTLS(certFile, keyFile)
	if err != nil {
		return fmt.Errorf("load TLS certs: %w", err)
	}

	if selectedTransport == "" || selectedTransport == "tls" {
		if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
		if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
	}
	if selectedTransport == "" || selectedTransport == "wss" {
		if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
			return err
		}
		time.Sleep(150 * time.Millisecond)
		return runWSSRequestsSecure(serverTLS, certFile)
	}
	return nil
}

func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, err
	}
	return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}

func runTCPSendPush() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	received := make(chan bool, 1)
	server := toki.NewTcpServer(host, goTypescriptTCPPort, func(conn net.Conn) *toki.TcpClient {
		return toki.NewTcpClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.TcpClient) {
		toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
			fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
			valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
			select {
			case received <- valid:
			default:
			}
			if valid {
				_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
			}
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	if err := runTypescriptClient("tcp", goTypescriptTCPPort, "send-push", map[string]bool{"1": true, "2": true}); err != nil {
		return err
	}
	select {
	case ok := <-received:
		if !ok {
			return errors.New("TCP send-push server received unexpected data")
		}
	case <-time.After(serverObservationWindow):
		return errors.New("TCP send-push server did not receive expected data")
	}
	return nil
}

func runTCPRequests() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	server := toki.NewTcpServer(host, goTypescriptTCPPort, func(conn net.Conn) *toki.TcpClient {
		return toki.NewTcpClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.TcpClient) {
		toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
			return &packets.TestData{
				Index:   req.GetIndex() * 2,
				Message: "echo: " + req.GetMessage(),
			}, nil
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	return runTypescriptClient("tcp", goTypescriptTCPPort, "requests", map[string]bool{"3": true, "4": true})
}

func runWSSendPush() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	received := make(chan bool, 1)
	server := toki.NewWsServer(host, goTypescriptWSPort, wsPath, func(conn *websocket.Conn) *toki.WsClient {
		return toki.NewWsClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.WsClient) {
		toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
			fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
			valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
			select {
			case received <- valid:
			default:
			}
			if valid {
				_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
			}
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	if err := runTypescriptClient("ws", goTypescriptWSPort, "send-push", map[string]bool{"1": true, "2": true}); err != nil {
		return err
	}
	select {
	case ok := <-received:
		if !ok {
			return errors.New("WS send-push server received unexpected data")
		}
	case <-time.After(serverObservationWindow):
		return errors.New("WS send-push server did not receive expected data")
	}
	return nil
}

func runWSRequests() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	server := toki.NewWsServer(host, goTypescriptWSPort, wsPath, func(conn *websocket.Conn) *toki.WsClient {
		return toki.NewWsClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.WsClient) {
		toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
			return &packets.TestData{
				Index:   req.GetIndex() * 2,
				Message: "echo: " + req.GetMessage(),
			}, nil
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	return runTypescriptClient("ws", goTypescriptWSPort, "requests", map[string]bool{"3": true, "4": true})
}

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

	received := make(chan bool, 1)
	server := toki.NewTcpServerTLS(host, goTypescriptTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
		return toki.NewTcpClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.TcpClient) {
		toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
			fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
			valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
			select {
			case received <- valid:
			default:
			}
			if valid {
				_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
			}
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	if err := runTypescriptClient("tls", goTypescriptTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
		return err
	}
	select {
	case ok := <-received:
		if !ok {
			return errors.New("TLS TCP send-push server received unexpected data")
		}
	case <-time.After(serverObservationWindow):
		return errors.New("TLS TCP send-push server did not receive expected data")
	}
	return nil
}

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

	server := toki.NewTcpServerTLS(host, goTypescriptTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
		return toki.NewTcpClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.TcpClient) {
		toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
			return &packets.TestData{
				Index:   req.GetIndex() * 2,
				Message: "echo: " + req.GetMessage(),
			}, nil
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	return runTypescriptClient("tls", goTypescriptTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}

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

	received := make(chan bool, 1)
	server := toki.NewWsServerTLS(host, goTypescriptWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
		return toki.NewWsClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.WsClient) {
		toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
			fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
			valid := data.GetIndex() == 101 && data.GetMessage() == "fire from typescript client"
			select {
			case received <- valid:
			default:
			}
			if valid {
				_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
			}
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	if err := runTypescriptClient("wss", goTypescriptWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
		return err
	}
	select {
	case ok := <-received:
		if !ok {
			return errors.New("WSS send-push server received unexpected data")
		}
	case <-time.After(serverObservationWindow):
		return errors.New("WSS send-push server did not receive expected data")
	}
	return nil
}

func runTCPLegacySimpleReceive() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	received := make(chan bool, 1)
	server := toki.NewTcpServer(host, goTypescriptTCPPort, func(conn net.Conn) *toki.TcpClient {
		return toki.NewTcpClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.TcpClient) {
		toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
			select {
			case received <- data.GetIndex() == 101:
			default:
			}
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	if err := runTypescriptClient("tcp", goTypescriptTCPPort, "legacy-receive", map[string]bool{}); err != nil {
		return err
	}
	select {
	case ok := <-received:
		if !ok {
			return errors.New("TCP legacy simple-name server received unexpected data")
		}
	case <-time.After(serverObservationWindow):
		return errors.New("TCP legacy simple-name server did not receive packet")
	}
	fmt.Println("PASS scenario=tcp-legacy-simple-receive detail=server-received-legacy-alias")
	return nil
}

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

	server := toki.NewWsServerTLS(host, goTypescriptWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
		return toki.NewWsClient(conn, 0, 0, parserMap())
	})
	server.OnClientConnected = func(client *toki.WsClient) {
		toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
			return &packets.TestData{
				Index:   req.GetIndex() * 2,
				Message: "echo: " + req.GetMessage(),
			}, nil
		})
	}
	if err := server.Start(ctx); err != nil {
		return err
	}
	defer server.Stop()

	return runTypescriptClient("wss", goTypescriptWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}

func runTypescriptClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
	ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
	defer cancel()

	typescriptDir, err := typescriptPackageDir()
	if err != nil {
		return err
	}

	tsxBin := filepath.Join(typescriptDir, "node_modules", ".bin", "tsx")
	args := []string{
		"crosstest/go_typescript_client.ts",
		"--mode=" + mode,
		fmt.Sprintf("--port=%d", port),
		"--phase=" + phase,
	}
	if len(certFile) > 0 && certFile[0] != "" {
		args = append(args, "--cert="+certFile[0])
	}
	cmd := exec.CommandContext(ctx, tsxBin, args...)
	cmd.Dir = typescriptDir
	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 mu sync.Mutex
	var resultLines []string
	var wg sync.WaitGroup
	wg.Add(2)
	go scanLines(&wg, stdoutPipe, os.Stdout, &mu, &resultLines)
	go scanLines(&wg, stderrPipe, os.Stderr, nil, nil)

	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("typescript client %s/%s pipe scan timed out", mode, phase)
	}
	if ctx.Err() == context.DeadlineExceeded {
		return fmt.Errorf("typescript client %s/%s timed out", mode, phase)
	}
	return validateResultLines("typescript-client "+mode+"/"+phase, waitErr, resultLines, expected)
}

func typescriptPackageDir() (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, "typescript"))
	}
	if wd, err := os.Getwd(); err == nil {
		candidates = append(candidates, findTypescriptPackageCandidates(wd)...)
	}
	if executable, err := os.Executable(); err == nil {
		candidates = append(candidates, findTypescriptPackageCandidates(filepath.Dir(executable))...)
	}

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

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

func isTypescriptPackageDir(dir string) bool {
	packageJSON, err := os.Stat(filepath.Join(dir, "package.json"))
	if err != nil || packageJSON.IsDir() {
		return false
	}
	tsconfig, err := os.Stat(filepath.Join(dir, "tsconfig.json"))
	return err == nil && !tsconfig.IsDir()
}

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()
}

func scanLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File, mu *sync.Mutex, resultLines *[]string) {
	defer wg.Done()
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		line := scanner.Text()
		fmt.Fprintln(writer, line)
		if mu != nil && startsWithResult(line) {
			mu.Lock()
			*resultLines = append(*resultLines, line)
			mu.Unlock()
		}
	}
}

func validateResultLines(label string, waitErr error, lines []string, expected map[string]bool) error {
	failed := make([]string, 0)
	passed := make(map[string]bool)
	re := regexp.MustCompile(`scenario=([^ ]+)`)
	for _, line := range lines {
		if len(line) >= 5 && line[:5] == "FAIL " {
			failed = append(failed, line)
			continue
		}
		if len(line) >= 5 && line[:5] == "PASS " {
			match := re.FindStringSubmatch(line)
			if len(match) == 2 {
				passed[match[1]] = true
			}
		}
	}

	missing := make([]string, 0)
	for scenario := range expected {
		if !passed[scenario] {
			missing = append(missing, scenario)
		}
	}
	if waitErr != nil || len(failed) > 0 || len(missing) > 0 {
		return fmt.Errorf("%s failed waitErr=%v failed=%v missing=%v", label, waitErr, failed, missing)
	}
	return nil
}

func startsWithResult(line string) bool {
	return (len(line) >= 5 && line[:5] == "PASS ") || (len(line) >= 5 && line[:5] == "FAIL ")
}
