package yamux

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

	"github.com/anyproto/any-sync/app"
	"github.com/anyproto/any-sync/net/secureservice"
	"github.com/anyproto/any-sync/net/transport"
	"github.com/anyproto/any-sync/nodeconf"
	"github.com/anyproto/any-sync/nodeconf/mock_nodeconf"
	"github.com/anyproto/any-sync/testutil/accounttest"
	"github.com/anyproto/any-sync/testutil/testnodeconf"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
)

var ctx = context.Background()

func TestYamuxTransport_Dial(t *testing.T) {
	fxS := newFixture(t)
	defer fxS.finish(t)
	fxC := newFixture(t)
	defer fxC.finish(t)

	mcC, err := fxC.Dial(ctx, fxS.addr)
	require.NoError(t, err)
	var mcS transport.MultiConn
	select {
	case mcS = <-fxS.accepter.mcs:
	case <-time.After(time.Second * 5):
		require.True(t, false, "timeout")
	}

	var (
		sData     string
		acceptErr error
		copyErr   error
		done      = make(chan struct{})
	)

	go func() {
		defer close(done)
		conn, serr := mcS.Accept()
		if serr != nil {
			acceptErr = serr
			return
		}
		buf := bytes.NewBuffer(nil)
		_, copyErr = io.Copy(buf, conn)
		sData = buf.String()
		return
	}()

	conn, err := mcC.Open(ctx)
	require.NoError(t, err)
	data := "some data"
	_, err = conn.Write([]byte(data))
	require.NoError(t, err)
	require.NoError(t, conn.Close())
	<-done

	assert.NoError(t, acceptErr)
	assert.Equal(t, data, sData)
	assert.NoError(t, copyErr)
}

// no deadline - 69100 rps
// common write deadline - 66700 rps
// subconn write deadline - 67100 rps
func TestWriteBenchReuse(t *testing.T) {
	t.Skip()
	var (
		numSubConn = 10
		numWrites  = 10000
	)

	fxS := newFixture(t)
	defer fxS.finish(t)
	fxC := newFixture(t)
	defer fxC.finish(t)

	mcC, err := fxC.Dial(ctx, fxS.addr)
	require.NoError(t, err)
	mcS := <-fxS.accepter.mcs

	go func() {
		for i := 0; i < numSubConn; i++ {
			conn, err := mcS.Accept()
			require.NoError(t, err)
			go func(sc net.Conn) {
				var b = make([]byte, 1024)
				for {
					n, _ := sc.Read(b)
					if n > 0 {
						sc.Write(b[:n])
					} else {
						break
					}
				}
			}(conn)
		}
	}()

	var wg sync.WaitGroup
	wg.Add(numSubConn)
	st := time.Now()
	for i := 0; i < numSubConn; i++ {
		conn, err := mcC.Open(ctx)
		require.NoError(t, err)
		go func(sc net.Conn) {
			defer sc.Close()
			defer wg.Done()
			for j := 0; j < numWrites; j++ {
				var b = []byte("some data some data some data some data some data some data some data some data some data")
				sc.Write(b)
				sc.Read(b)
			}
		}(conn)
	}
	wg.Wait()
	dur := time.Since(st)
	t.Logf("%.2f req per sec", float64(numWrites*numSubConn)/dur.Seconds())
}

func TestWriteBenchNew(t *testing.T) {
	t.Skip()
	var (
		numSubConn = 10
		numWrites  = 10000
	)

	fxS := newFixture(t)
	defer fxS.finish(t)
	fxC := newFixture(t)
	defer fxC.finish(t)

	mcC, err := fxC.Dial(ctx, fxS.addr)
	require.NoError(t, err)
	mcS := <-fxS.accepter.mcs

	go func() {
		for i := 0; i < numSubConn; i++ {
			require.NoError(t, err)
			go func() {
				var b = make([]byte, 1024)
				for {
					conn, _ := mcS.Accept()
					n, _ := conn.Read(b)
					if n > 0 {
						conn.Write(b[:n])
					} else {
						_ = conn.Close()
						break
					}
					conn.Close()
				}
			}()
		}
	}()

	var wg sync.WaitGroup
	wg.Add(numSubConn)
	st := time.Now()
	for i := 0; i < numSubConn; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < numWrites; j++ {
				sc, err := mcC.Open(ctx)
				require.NoError(t, err)
				var b = []byte("some data some data some data some data some data some data some data some data some data")
				sc.Write(b)
				sc.Read(b)
				sc.Close()
			}
		}()
	}
	wg.Wait()
	dur := time.Since(st)
	t.Logf("%.2f req per sec", float64(numWrites*numSubConn)/dur.Seconds())
}

type fixture struct {
	*yamuxTransport
	a            *app.App
	ctrl         *gomock.Controller
	mockNodeConf *mock_nodeconf.MockService
	acc          *accounttest.AccountTestService
	accepter     *testAccepter
	addr         string
}

func newFixture(t *testing.T) *fixture {
	fx := &fixture{
		yamuxTransport: New().(*yamuxTransport),
		ctrl:           gomock.NewController(t),
		acc:            &accounttest.AccountTestService{},
		accepter:       &testAccepter{mcs: make(chan transport.MultiConn, 100)},
		a:              new(app.App),
	}

	fx.mockNodeConf = mock_nodeconf.NewMockService(fx.ctrl)
	fx.mockNodeConf.EXPECT().Init(gomock.Any())
	fx.mockNodeConf.EXPECT().Name().Return(nodeconf.CName).AnyTimes()
	fx.mockNodeConf.EXPECT().Run(ctx)
	fx.mockNodeConf.EXPECT().Close(ctx)
	fx.mockNodeConf.EXPECT().NodeTypes(gomock.Any()).Return([]nodeconf.NodeType{nodeconf.NodeTypeTree}).AnyTimes()
	fx.a.Register(fx.acc).Register(newTestConf()).Register(fx.mockNodeConf).Register(secureservice.New()).Register(fx.yamuxTransport).Register(fx.accepter)
	require.NoError(t, fx.a.Start(ctx))
	fx.addr = fx.listeners[0].Addr().String()
	return fx
}

func (fx *fixture) finish(t *testing.T) {
	require.NoError(t, fx.a.Close(ctx))
	fx.ctrl.Finish()
}

func newTestConf() *testConf {
	return &testConf{testnodeconf.GenNodeConfig(1)}
}

type testConf struct {
	*testnodeconf.Config
}

func (c *testConf) GetYamux() Config {
	return Config{
		ListenAddrs:     []string{"127.0.0.1:0"},
		WriteTimeoutSec: 10,
		DialTimeoutSec:  10,
	}
}

type testAccepter struct {
	err error
	mcs chan transport.MultiConn
}

func (t *testAccepter) Accept(mc transport.MultiConn) (err error) {
	t.mcs <- mc
	return t.err
}

func (t *testAccepter) Init(a *app.App) (err error) {
	a.MustComponent(CName).(transport.Transport).SetAccepter(t)
	return nil
}

func (t *testAccepter) Name() (name string) { return "testAccepter" }

// TestDialContextCancellation tests that canceling the dial context aborts the dial quickly
func TestDialContextCancellation(t *testing.T) {
	fx := newFixture(t)
	defer fx.finish(t)

	// Create a context that we'll cancel immediately
	ctx, cancel := context.WithCancel(context.Background())

	// Use a non-routable address to ensure the connection will hang
	addr := "192.0.2.1:12345" // TEST-NET-1, guaranteed not to be routable

	// Start the dial in a goroutine
	done := make(chan error)
	go func() {
		_, err := fx.Dial(ctx, addr)
		done <- err
	}()

	// Give the dial a moment to start
	time.Sleep(100 * time.Millisecond)

	// Cancel the context
	cancel()

	// The dial should return quickly with an error
	select {
	case err := <-done:
		require.Error(t, err)
		// Check that the error is related to cancellation
		errStr := err.Error()
		assert.True(t,
			strings.Contains(errStr, "context") ||
				strings.Contains(errStr, "canceled") ||
				strings.Contains(errStr, "operation was canceled"),
			"Expected cancellation error, got: %s", errStr)
	case <-time.After(2 * time.Second):
		t.Fatal("Dial did not abort quickly when context was cancelled")
	}
}
