package tunnel

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/ooni/probe-cli/v3/internal/atomicx"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/model/mocks"
	"github.com/ooni/probe-cli/v3/internal/ptx"
)

type torsfPTXListenerWrapper struct {
	torsfPTXListener
	counter *atomicx.Int64
}

func (tw *torsfPTXListenerWrapper) Stop() {
	tw.counter.Add(1)
	tw.torsfPTXListener.Stop()
}

func Test_torsfStart(t *testing.T) {
	t.Run("newSnowflakeDialer fails", func(t *testing.T) {
		ctx := context.Background()
		config := &Config{
			Name:                 "torsf",
			Session:              &MockableSession{},
			SnowflakeRendezvous:  "antani", // should cause failure
			TunnelDir:            filepath.Join(os.TempDir(), "torsf-xx"),
			Logger:               model.DiscardLogger,
			TorArgs:              []string{},
			TorBinary:            "",
			testExecabsLookPath:  nil,
			testMkdirAll:         nil,
			testNetListen:        nil,
			testSocks5New:        nil,
			testTorStart:         nil,
			testTorProtocolInfo:  nil,
			testTorEnableNetwork: nil,
			testTorGetInfo:       nil,
		}
		expectDebugInfo := DebugInfo{}
		tun, debugInfo, err := torsfStart(ctx, config)
		if !errors.Is(err, ptx.ErrSnowflakeNoSuchRendezvousMethod) {
			t.Fatal("unexpected err", err)
		}
		if tun != nil {
			t.Fatal("expected nil tun")
		}
		if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("ptl.Start fails", func(t *testing.T) {
		ctx := context.Background()
		expected := errors.New("mocked error")
		config := &Config{
			Name:                "torsf",
			Session:             &MockableSession{},
			SnowflakeRendezvous: "", // is the default
			TunnelDir:           filepath.Join(os.TempDir(), "torsf-xx"),
			Logger:              model.DiscardLogger,
			TorArgs:             []string{},
			TorBinary:           "",
			testExecabsLookPath: nil,
			testMkdirAll:        nil,
			testNetListen:       nil,
			testSfListenSocks: func(network, laddr string) (ptx.SocksListener, error) {
				return nil, expected
			},
			testSocks5New:        nil,
			testTorStart:         nil,
			testTorProtocolInfo:  nil,
			testTorEnableNetwork: nil,
			testTorGetInfo:       nil,
		}
		expectDebugInfo := DebugInfo{}
		tun, debugInfo, err := torsfStart(ctx, config)
		if !errors.Is(err, expected) {
			t.Fatal("unexpected err", err)
		}
		if tun != nil {
			t.Fatal("expected nil tun")
		}
		if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("torStart fails", func(t *testing.T) {
		ctx := context.Background()
		stopCounter := &atomicx.Int64{}
		expected := errors.New("expected err")
		config := &Config{
			Name:                "torsf",
			Session:             &MockableSession{},
			SnowflakeRendezvous: "", // is the default
			TunnelDir:           filepath.Join(os.TempDir(), "torsf-xx"),
			Logger:              model.DiscardLogger,
			TorArgs:             []string{},
			TorBinary:           "",
			testExecabsLookPath: nil,
			testMkdirAll:        nil,
			testNetListen:       nil,
			testSfListenSocks:   nil,
			testSfWrapPTXListener: func(tp torsfPTXListener) torsfPTXListener {
				return &torsfPTXListenerWrapper{
					torsfPTXListener: tp,
					counter:          stopCounter,
				}
			},
			testSfTorStart: func(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
				return nil, DebugInfo{}, expected
			},
			testSocks5New:        nil,
			testTorStart:         nil,
			testTorProtocolInfo:  nil,
			testTorEnableNetwork: nil,
			testTorGetInfo:       nil,
		}
		expectDebugInfo := DebugInfo{
			Name: "torsf",
		}
		tun, debugInfo, err := torsfStart(ctx, config)
		if !errors.Is(err, expected) {
			t.Fatal("unexpected err", err)
		}
		if tun != nil {
			t.Fatal("expected nil tun")
		}
		if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
			t.Fatal(diff)
		}
		if stopCounter.Load() != 1 {
			t.Fatal("did not call stop")
		}
	})

	t.Run("on success", func(t *testing.T) {
		ctx := context.Background()
		expectDebugInfo := DebugInfo{
			Name: "torsf",
		}
		config := &Config{
			Name:                "torsf",
			Session:             &MockableSession{},
			SnowflakeRendezvous: "", // is the default
			TunnelDir:           filepath.Join(os.TempDir(), "torsf-xx"),
			Logger:              model.DiscardLogger,
			TorArgs:             []string{},
			TorBinary:           "",
			testExecabsLookPath: nil,
			testMkdirAll:        nil,
			testNetListen:       nil,
			testSfListenSocks:   nil,
			testSfTorStart: func(ctx context.Context, config *Config) (Tunnel, DebugInfo, error) {
				tun := &fakeTunnel{
					addr: &mocks.Addr{
						MockString: func() string {
							return "127.0.0.1:5555"
						},
					},
					bootstrapTime: 123,
					listener: &mocks.Listener{
						MockClose: func() error {
							return nil
						},
					},
					once: sync.Once{},
				}
				return tun, expectDebugInfo, nil
			},
			testSocks5New:        nil,
			testTorStart:         nil,
			testTorProtocolInfo:  nil,
			testTorEnableNetwork: nil,
			testTorGetInfo:       nil,
		}
		tun, debugInfo, err := torsfStart(ctx, config)
		if err != nil {
			t.Fatal(err)
		}
		if diff := cmp.Diff(expectDebugInfo, debugInfo); diff != "" {
			t.Fatal(diff)
		}
		if tun.BootstrapTime() != 123 {
			t.Fatal("invalid bootstrap time")
		}
		if tun.SOCKS5ProxyURL().String() != "socks5://127.0.0.1:5555" {
			t.Fatal("invalid socks5 proxy URL")
		}
		tun.Stop()
	})
}
