package redirect

import (
	"context"
	"io"
	"net"
	"sync"
	"testing"
	"time"

	"git.okcu.io/root/drover-go/internal/socks5"
	"github.com/stretchr/testify/require"
)

// startEchoListener spins up a TCP server that echoes whatever it reads.
func startEchoListener(t *testing.T) string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	t.Cleanup(func() { ln.Close() })
	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			go func(c net.Conn) {
				defer c.Close()
				io.Copy(c, c)
			}(c)
		}
	}()
	return ln.Addr().String()
}

// startFakeSOCKS5 returns the addr of a no-auth SOCKS5 server that
// CONNECT-tunnels to the requested host:port. (Borrowed pattern from
// internal/socks5/client_test.go.)
func startFakeSOCKS5(t *testing.T) string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	t.Cleanup(func() { ln.Close() })
	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			go func(c net.Conn) {
				defer c.Close()
				buf := make([]byte, 256)
				// Greet
				io.ReadFull(c, buf[:2])
				nm := int(buf[1])
				io.ReadFull(c, buf[:nm])
				c.Write([]byte{0x05, 0x00})
				// CONNECT
				io.ReadFull(c, buf[:4])
				atyp := buf[3]
				var host string
				switch atyp {
				case 1:
					io.ReadFull(c, buf[:4])
					host = net.IPv4(buf[0], buf[1], buf[2], buf[3]).String()
				case 3:
					io.ReadFull(c, buf[:1])
					hl := int(buf[0])
					io.ReadFull(c, buf[:hl])
					host = string(buf[:hl])
				}
				io.ReadFull(c, buf[:2])
				port := int(buf[0])<<8 | int(buf[1])
				c.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
				up, err := net.Dial("tcp", net.JoinHostPort(host, sportItoa(port)))
				if err != nil {
					return
				}
				defer up.Close()
				var wg sync.WaitGroup
				wg.Add(2)
				go func() { defer wg.Done(); io.Copy(up, c) }()
				go func() { defer wg.Done(); io.Copy(c, up) }()
				wg.Wait()
			}(c)
		}
	}()
	return ln.Addr().String()
}

func sportItoa(n int) string {
	if n == 0 {
		return "0"
	}
	out := []byte{}
	for n > 0 {
		out = append([]byte{byte('0' + n%10)}, out...)
		n /= 10
	}
	return string(out)
}

func TestRedirector_PipesEcho(t *testing.T) {
	echoAddr := startEchoListener(t)
	echoHost, echoPortStr, _ := net.SplitHostPort(echoAddr)
	echoPort := parseU16(echoPortStr)
	socksAddr := startFakeSOCKS5(t)

	r, err := New(Config{
		SOCKS5: socks5.Config{ProxyAddr: socksAddr},
		Bind:   "127.0.0.1:0",
	})
	require.NoError(t, err)
	t.Cleanup(func() { r.Close() })

	// Manually map: pretend a packet from src_port=12345 was destined to echo.
	r.SetMapping(12345, net.ParseIP(echoHost), echoPort)

	// Dial the redirector listener using src_port=12345 so it looks
	// up the mapping correctly.
	d := net.Dialer{LocalAddr: &net.TCPAddr{Port: 12345}}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	conn, err := d.DialContext(ctx, "tcp", r.LocalAddr())
	require.NoError(t, err)
	defer conn.Close()

	conn.Write([]byte("ping"))
	conn.SetReadDeadline(time.Now().Add(time.Second))
	buf := make([]byte, 4)
	io.ReadFull(conn, buf)
	require.Equal(t, "ping", string(buf))
}

func parseU16(s string) uint16 {
	var n int
	for _, c := range s {
		n = n*10 + int(c-'0')
	}
	return uint16(n)
}
