package pubsubclient

import (
	"crypto/tls"
	"sync"
	"testing"
	"time"

	"github.com/gorilla/websocket"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var testTLSConf = &tls.Config{InsecureSkipVerify: true}

func TestSendPing(t *testing.T) {
	pubsub, server := runMockPubsubServer()
	defer pubsub.wg.Wait()
	defer server.Close()

	dialer := &websocket.Dialer{TLSClientConfig: testTLSConf}
	socket, _, err := dialer.Dial(server.URL, nil)
	require.NoError(t, err)

	conn := newConn(socket)
	defer func() {
		assert.NoError(t, conn.close())
	}()

	conn.wg.Add(1)
	go conn.sendLoop()

	conn.sendPing()

	have := getMsgTimeout(t, pubsub.recv, time.Second)
	assert.Equal(t, &pingMsg{msg{ping}}, have)
}

func TestConnPingPong(t *testing.T) {
	pubsub, server := runMockPubsubServer()
	defer pubsub.wg.Wait()
	defer server.Close()

	dialer := &websocket.Dialer{TLSClientConfig: testTLSConf}
	socket, _, err := dialer.Dial(server.URL, nil)
	require.NoError(t, err)

	conn := newConn(socket)
	conn.pongTimeout = 50 * time.Millisecond

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		conn.run()
		wg.Done()
	}()

	conn.sendPing()

	have := getMsgTimeout(t, pubsub.recv, time.Second)
	assert.Equal(t, &pingMsg{msg{ping}}, have)

	pubsub.sendMsg(&pongMsg{msg{pong}})

	time.Sleep(3 * conn.pongTimeout)

	assert.NoError(t, conn.close())
	wg.Wait()
	select {
	case err = <-conn.errors():
	default:
	}
	assert.NoError(t, err)
}

func TestConnPingPongTimeout(t *testing.T) {
	pubsub, server := runMockPubsubServer()
	defer pubsub.wg.Wait()
	defer server.Close()

	dialer := &websocket.Dialer{TLSClientConfig: testTLSConf}
	socket, _, err := dialer.Dial(server.URL, nil)
	require.NoError(t, err)

	conn := newConn(socket)
	conn.pongTimeout = 50 * time.Millisecond

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		conn.run()
		wg.Done()
	}()

	conn.sendPing()

	have := getMsgTimeout(t, pubsub.recv, time.Second)
	assert.Equal(t, &pingMsg{msg{ping}}, have)
	time.Sleep(3 * conn.pongTimeout)

	wg.Wait()
	select {
	case err = <-conn.errors():
	default:
	}
	require.Error(t, err)
	assert.Equal(t, "timed out while waiting for pong", err.Error())
	assert.NoError(t, conn.close())
}

// Do a LISTEN request; get a response with the wrong nonce
func TestListenNonceChecking(t *testing.T) {
	pubsub, server := runMockPubsubServer()
	defer pubsub.wg.Wait()
	defer server.Close()

	dialer := &websocket.Dialer{TLSClientConfig: testTLSConf}
	socket, _, err := dialer.Dial(server.URL, nil)
	require.NoError(t, err)

	conn := newConn(socket)
	defer func() {
		assert.NoError(t, conn.close())
	}()
	conn.responseTimeout = 50 * time.Millisecond

	go conn.run()

	l := &listenMsg{
		msg:   msg{listen},
		Nonce: "123",
		Data: topicList{
			Topics: []string{"topic"},
		},
	}

	sendResult := make(chan error)
	go func() {
		sendResult <- conn.send(l)
	}()

	have := getMsgTimeout(t, pubsub.recv, time.Second)
	assert.Equal(t, l, have)

	pubsub.sendMsg(&responseMsg{msg: msg{response}, Nonce: "456"})

	select {
	case err := <-sendResult:
		require.Error(t, err)
		assert.Equal(t, "response timed out", err.Error())
	case <-time.After(20 * conn.responseTimeout):
		require.Fail(t, "test timed out")
	}
}

// Do a LISTEN request; get a response with the wrong nonce, then the right one
func TestListenMultipleResponses(t *testing.T) {
	pubsub, server := runMockPubsubServer()
	defer pubsub.wg.Wait()
	defer server.Close()

	dialer := &websocket.Dialer{TLSClientConfig: testTLSConf}
	socket, _, err := dialer.Dial(server.URL, nil)
	require.NoError(t, err)

	conn := newConn(socket)
	defer func() {
		assert.NoError(t, conn.close())
	}()
	conn.responseTimeout = 50 * time.Millisecond

	go conn.run()

	l := &listenMsg{
		msg:   msg{listen},
		Nonce: "123",
		Data: topicList{
			Topics: []string{"topic"},
		},
	}

	sendResult := make(chan error)
	go func() {
		sendResult <- conn.send(l)
	}()

	have := getMsgTimeout(t, pubsub.recv, time.Second)
	assert.Equal(t, l, have)

	pubsub.sendMsg(&responseMsg{msg: msg{response}, Nonce: "456"})
	pubsub.sendMsg(&responseMsg{msg: msg{response}, Nonce: "123"})

	select {
	case err := <-sendResult:
		require.NoError(t, err)
	case <-time.After(20 * conn.responseTimeout):
		require.Fail(t, "test timed out")
	}
}

func TestForwardToNil(t *testing.T) {
	c := newConn(nil)
	c.forwardTo(nil)
	assert.NotPanics(t, func() {
		c.recvMsg(&Message{})
	})
}

// Get a message within a timeout. Fail the test if the timeout expires.
func getMsgTimeout(t *testing.T, ch chan typedMessage, timeout time.Duration) typedMessage {
	select {
	case msg := <-ch:
		return msg
	case <-time.After(timeout):
		assert.Fail(t, "did not receive a message within timeout")
		return nil
	}
}
