package server

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/FrankerFaceZ/FrankerFaceZ/socketserver/server/rate"
	"github.com/gorilla/websocket"
	"github.com/satori/go.uuid"
)

const TestOrigin = "http://www.twitch.tv"

func TestSubscriptionAndPublish(t *testing.T) {
	var doneWg sync.WaitGroup
	var readyWg sync.WaitGroup

	const TestChannelName1 = "room.testchannel"
	const TestChannelName2 = "room.chan2"
	const TestChannelName3 = "room.chan3"
	const TestChannelNameUnused = "room.empty"
	const TestCommandChan = "testdata_single"
	const TestCommandMulti = "testdata_multi"
	const TestCommandGlobal = "testdata_global"
	const TestData1 = "123456789"
	const TestData2 = 42
	const TestData3 = false
	var TestData4 = []interface{}{"str1", "str2", "str3"}

	t.Log("TestSubscriptionAndPublish")

	var server *httptest.Server
	var urls TURLs

	var backendExpected = NewTBackendRequestChecker(t,
		TExpectedBackendRequest{200, bPathAnnounceStartup, &url.Values{"startup": []string{"1"}}, "", nil},
		TExpectedBackendRequest{200, bPathAddTopic, &url.Values{"channels": []string{TestChannelName1}, "added": []string{"t"}}, "ok", nil},
		TExpectedBackendRequest{200, bPathAddTopic, &url.Values{"channels": []string{TestChannelName2}, "added": []string{"t"}}, "ok", nil},
		TExpectedBackendRequest{200, bPathAddTopic, &url.Values{"channels": []string{TestChannelName3}, "added": []string{"t"}}, "ok", nil},
	)
	server, _, urls = TSetup(SetupWantSocketServer|SetupWantBackendServer|SetupWantURLs, backendExpected)

	defer server.CloseClientConnections()
	defer unsubscribeAllClients()
	defer backendExpected.Close()

	var conn *websocket.Conn
	var resp *http.Response
	var err error

	var headers http.Header = make(http.Header)
	headers.Set("Origin", TestOrigin)

	// client 1: sub ch1, ch2
	// client 2: sub ch1, ch3
	// client 3: sub none
	// client 4: delayed sub ch1
	// msg 1: ch1
	// msg 2: ch2, ch3
	// msg 3: chEmpty
	// msg 4: global uncached

	// Client 1
	conn, resp, err = websocket.DefaultDialer.Dial(urls.Websocket, headers)
	if err != nil {
		t.Error(err)
		return
	}

	// both origins need testing
	headers.Set("Origin", "https://www.twitch.tv")

	doneWg.Add(1)
	readyWg.Add(1)
	go func(conn *websocket.Conn) {
		TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
		TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments)
		TSendMessage(t, conn, 2, "sub", TestChannelName1)
		TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil)
		TSendMessage(t, conn, 3, "sub", TestChannelName2) // 2
		TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil)
		TSendMessage(t, conn, 4, "ready", 0)
		TReceiveExpectedMessage(t, conn, 4, SuccessCommand, nil)

		readyWg.Done()

		TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1)
		TReceiveExpectedMessage(t, conn, -1, TestCommandMulti, TestData2)
		TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4)

		conn.Close()
		doneWg.Done()
	}(conn)

	// Client 2
	conn, resp, err = websocket.DefaultDialer.Dial(urls.Websocket, headers)
	if err != nil {
		t.Error(err)
		return
	}

	doneWg.Add(1)
	readyWg.Wait() // enforce ordering
	readyWg.Add(1)
	go func(conn *websocket.Conn) {
		TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
		TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments)
		TSendMessage(t, conn, 2, "sub", TestChannelName1)
		TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil)
		TSendMessage(t, conn, 3, "sub", TestChannelName3) // 3
		TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil)
		TSendMessage(t, conn, 4, "ready", 0)
		TReceiveExpectedMessage(t, conn, 4, SuccessCommand, nil)

		readyWg.Done()

		TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1)
		TReceiveExpectedMessage(t, conn, -1, TestCommandMulti, TestData2)
		TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4)

		conn.Close()
		doneWg.Done()
	}(conn)

	// Client 3
	conn, resp, err = websocket.DefaultDialer.Dial(urls.Websocket, headers)
	if err != nil {
		t.Error(err)
		return
	}

	doneWg.Add(1)
	readyWg.Wait() // enforce ordering
	time.Sleep(2 * time.Millisecond)
	readyWg.Add(1)
	go func(conn *websocket.Conn) {
		TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
		TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments)
		TSendMessage(t, conn, 2, "ready", 0)
		TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil)

		readyWg.Done()

		TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4)

		conn.Close()
		doneWg.Done()
	}(conn)

	// Wait for clients 1-3
	readyWg.Wait()

	var form url.Values

	// Publish message 1 - should go to clients 1, 2

	form, err = TSealForSavePubMsg(t, TestCommandChan, TestChannelName1, TestData1, false)
	if err != nil {
		t.FailNow()
	}
	resp, err = http.PostForm(urls.SavePubMsg, form)
	if !TCheckResponse(t, resp, strconv.Itoa(2), "pub msg 1") {
		t.FailNow()
	}

	// Publish message 2 - should go to clients 1, 2

	form, err = TSealForSavePubMsg(t, TestCommandMulti, TestChannelName2+","+TestChannelName3, TestData2, false)
	if err != nil {
		t.FailNow()
	}
	resp, err = http.PostForm(urls.SavePubMsg, form)
	if !TCheckResponse(t, resp, strconv.Itoa(2), "pub msg 2") {
		t.FailNow()
	}

	// Publish message 3 - should go to no clients

	form, err = TSealForSavePubMsg(t, TestCommandChan, TestChannelNameUnused, TestData3, false)
	if err != nil {
		t.FailNow()
	}
	resp, err = http.PostForm(urls.SavePubMsg, form)
	if !TCheckResponse(t, resp, strconv.Itoa(0), "pub msg 3") {
		t.FailNow()
	}

	// Publish message 4 - should go to clients 1, 2, 3

	form, err = TSealForUncachedPubMsg(t, TestCommandGlobal, "", TestData4, "global", false)
	if err != nil {
		t.FailNow()
	}
	resp, err = http.PostForm(urls.UncachedPubMsg, form)
	if !TCheckResponse(t, resp, strconv.Itoa(3), "pub msg 4") {
		t.FailNow()
	}

	// Start client 4
	conn, resp, err = websocket.DefaultDialer.Dial(urls.Websocket, headers)
	if err != nil {
		t.Error(err)
		return
	}

	doneWg.Add(1)
	readyWg.Add(1)
	go func(conn *websocket.Conn) {
		TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
		TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments)
		TSendMessage(t, conn, 2, "sub", TestChannelName1)
		TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil)
		TSendMessage(t, conn, 3, "ready", 0)
		TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil)

		// backlog message
		TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1)

		readyWg.Done()

		conn.Close()
		doneWg.Done()
	}(conn)

	readyWg.Wait()

	doneWg.Wait()
	server.Close()

	clientCount := readCurrentHLL()
	if clientCount < 3 || clientCount > 5 {
		t.Error("clientCount outside acceptable range: expected 4, got ", clientCount)
	}
}

func TestRestrictedCommands(t *testing.T) {
	var doneWg sync.WaitGroup
	var readyWg sync.WaitGroup

	const TestCommandNeedsAuth = "needsauth"
	const TestRequestData = "123456"
	const TestRequestDataJSON = "\"" + TestRequestData + "\""
	const TestReplyData = "success"
	const TestUsername = "sirstendec"

	var server *httptest.Server
	var urls TURLs

	t.Log("TestRestrictedCommands")

	var backendExpected = NewTBackendRequestChecker(t,
		TExpectedBackendRequest{200, bPathAnnounceStartup, &url.Values{"startup": []string{"1"}}, "", nil},
		TExpectedBackendRequest{401, fmt.Sprintf("%s%s", bPathOtherCommand, TestCommandNeedsAuth), &url.Values{"authenticated": []string{"0"}, "username": []string{""}, "clientData": []string{TestRequestDataJSON}}, "", nil},
		TExpectedBackendRequest{401, fmt.Sprintf("%s%s", bPathOtherCommand, TestCommandNeedsAuth), &url.Values{"authenticated": []string{"0"}, "username": []string{TestUsername}, "clientData": []string{TestRequestDataJSON}}, "", nil},
		TExpectedBackendRequest{200, fmt.Sprintf("%s%s", bPathOtherCommand, TestCommandNeedsAuth), &url.Values{"authenticated": []string{"1"}, "username": []string{TestUsername}, "clientData": []string{TestRequestDataJSON}}, fmt.Sprintf("\"%s\"", TestReplyData), nil},
	)
	server, _, urls = TSetup(SetupWantSocketServer|SetupWantBackendServer|SetupWantURLs, backendExpected)

	defer server.CloseClientConnections()
	defer unsubscribeAllClients()
	defer backendExpected.Close()

	var conn *websocket.Conn
	var err error
	var challengeChan = make(chan string)

	var headers http.Header = make(http.Header)
	headers.Set("Origin", TestOrigin)

	// Client 1
	conn, _, err = websocket.DefaultDialer.Dial(urls.Websocket, headers)
	if err != nil {
		t.Error(err)
		return
	}

	doneWg.Add(1)
	readyWg.Add(1)
	go func(conn *websocket.Conn) {
		defer doneWg.Done()
		defer conn.Close()
		TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
		TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments)
		TSendMessage(t, conn, 2, ReadyCommand, 0)
		TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil)

		// Should get immediate refusal because no username set
		TSendMessage(t, conn, 3, TestCommandNeedsAuth, TestRequestData)
		TReceiveExpectedMessage(t, conn, 3, ErrorCommand, AuthorizationNeededError)

		// Set a username
		TSendMessage(t, conn, 4, SetUserCommand, TestUsername)
		TReceiveExpectedMessage(t, conn, 4, SuccessCommand, nil)

		// Should get authorization prompt
		TSendMessage(t, conn, 5, TestCommandNeedsAuth, TestRequestData)
		readyWg.Done()
		msg, success := TReceiveExpectedMessage(t, conn, -1, AuthorizeCommand, IgnoreReceivedArguments)
		if !success {
			t.Error("recieve authorize command failed, cannot continue")
			return
		}
		challenge, err := msg.ArgumentsAsString()
		if err != nil {
			t.Error(err)
			return
		}
		challengeChan <- challenge // mocked: sending challenge to IRC server, IRC server sends challenge to socket server

		TReceiveExpectedMessage(t, conn, 5, SuccessCommand, TestReplyData)
	}(conn)

	readyWg.Wait()

	challenge := <-challengeChan
	PendingAuthLock.Lock()
	found := false
	for _, v := range PendingAuths {
		if conn.LocalAddr().String() == v.Client.RemoteAddr.String() {
			found = true
			if v.Challenge != challenge {
				t.Error("Challenge in array was not what client got")
			}
			break
		}
	}
	PendingAuthLock.Unlock()
	if !found {
		t.Fatal("Did not find authorization challenge in the pending auths array")
	}

	submitAuth(TestUsername, challenge)

	doneWg.Wait()
	server.Close()
}

func BenchmarkUserSubscriptionSinglePublish(b *testing.B) {
	var doneWg sync.WaitGroup
	var readyWg sync.WaitGroup

	const TestChannelName = "room.testchannel"
	const TestCommand = "testdata"
	const TestData = "123456789"

	message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: TestData}

	fmt.Println()
	fmt.Println(b.N)

	var limit syscall.Rlimit
	syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit)

	limit.Cur = TCountOpenFDs() + uint64(b.N)*2 + 100

	if limit.Cur > limit.Max {
		b.Skip("Open file limit too low")
		return
	}

	syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit)

	var server *httptest.Server
	var urls TURLs
	server, _, urls = TSetup(SetupWantSocketServer|SetupWantURLs, nil)
	defer unsubscribeAllClients()

	var headers http.Header = make(http.Header)
	headers.Set("Origin", TestOrigin)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		conn, _, err := websocket.DefaultDialer.Dial(urls.Websocket, headers)
		if err != nil {
			b.Error(err)
			break
		}
		doneWg.Add(1)
		readyWg.Add(1)
		go func(i int, conn *websocket.Conn) {
			TSendMessage(b, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()})
			TSendMessage(b, conn, 2, "sub", TestChannelName)

			TReceiveExpectedMessage(b, conn, 1, SuccessCommand, IgnoreReceivedArguments)
			TReceiveExpectedMessage(b, conn, 2, SuccessCommand, nil)

			readyWg.Done()

			TReceiveExpectedMessage(b, conn, -1, TestCommand, TestData)

			conn.Close()
			doneWg.Done()
		}(i, conn)
	}

	readyWg.Wait()

	fmt.Println("publishing...")
	if PublishToChannel(TestChannelName, message, rate.Unlimited()) != b.N {
		b.Error("not enough sent")
		server.CloseClientConnections()
		panic("halting test instead of waiting")
	}
	doneWg.Wait()
	fmt.Println("...done.")

	b.StopTimer()
	server.Close()
	server.CloseClientConnections()
}
