package arthur

import (
	"io/ioutil"
	"net"
	"os"
	"testing"
	"time"

	"git.csclub.uwaterloo.ca/public/merlin/config"
	"git.csclub.uwaterloo.ca/public/merlin/logger"
)

// Test that GetCommand is able to accept a connection and parse a request for the status
func TestStatusCommand(t *testing.T) {
	r, w := net.Pipe()

	go func() {
		// will only finish write when EOF is sent
		// only way to send EOF is to close
		w.Write([]byte("status"))
		w.Close()
	}()
	command, repoName := GetCommand(r)
	if command != "status" {
		t.Errorf("command for status should be \"status\", got " + command)
	} else if repoName != "" {
		t.Errorf("status should return an empty string for the repoName, got " + repoName)
	}
}

// Test that GetCommand is able to accept a connection and parse a request for the a forced sync
func TestSyncCommand(t *testing.T) {
	r, w := net.Pipe()

	go func() {
		w.Write([]byte("sync:ubuntu"))
		w.Close()
	}()
	command, repoName := GetCommand(r)
	r.Close()
	if command != "sync" {
		t.Errorf("command for sync:ubuntu should be \"sync\", got " + command)
	} else if repoName != "ubuntu" {
		t.Errorf("name of repo for sync:ubuntu should be \"ubuntu\", got " + repoName)
	}
}

// Test that SendStatus returns the correct status for some repo state
func TestSendStatus(t *testing.T) {
	saveRepoMap := config.RepoMap
	defer func() {
		config.RepoMap = saveRepoMap
	}()

	repoMap := make(map[string]*config.Repo)
	repoMap["eeeee"] = &config.Repo{
		Frequency: 30 * 86400,
		State: &config.RepoState{
			IsRunning:            true,
			LastAttemptStartTime: 1600000000,
		},
	}
	repoMap["alinux"] = &config.Repo{
		Frequency: 7*86400 + 3,
		State: &config.RepoState{
			IsRunning:            true,
			LastAttemptStartTime: 1620000000,
		},
	}
	repoMap["lnux"] = &config.Repo{
		Frequency: 86400,
		State: &config.RepoState{
			IsRunning:            false,
			LastAttemptStartTime: 1640000000,
		},
	}
	config.RepoMap = repoMap

	r, w := net.Pipe()
	go func() {
		SendStatus(w)
		w.Close()
	}()
	msg, err := ioutil.ReadAll(r)
	r.Close()
	if err != nil {
		t.Errorf(err.Error())
	}
	expected := `Repository     Last Synced                       Next Expected Sync                Running
alinux         Sun, 02 May 2021 20:00:00 EDT     Sun, 09 May 2021 20:00:03 EDT     true
eeeee          Sun, 13 Sep 2020 08:26:40 EDT     Tue, 13 Oct 2020 08:26:40 EDT     true
lnux           Mon, 20 Dec 2021 06:33:20 EST     Tue, 21 Dec 2021 06:33:20 EST     false
`
	if expected != string(msg) {
		t.Errorf("Expected:\n" + expected + "\nGot:\n" + string(msg))
	}
}

// Test that ForceSync behaves properly
func TestForceSync(t *testing.T) {
	saveRepos := config.Repos
	saveRepoMap := config.RepoMap
	doneChan := make(chan config.SyncResult)
	defer func() {
		config.Repos = saveRepos
		config.RepoMap = saveRepoMap
		close(doneChan)
	}()

	// Part 1: run a dummy sync
	repo := config.Repo{
		Name:      "nux",
		SyncType:  "csc-sync-dummy",
		Frequency: 7 * 86400,
		MaxTime:   30,
		Logger:    logger.NewLogger("nux", "/tmp/merlin_force_sync_test_log", false),
		StateFile: "/tmp/merlin_force_sync_test_state",
		DoneChan:  doneChan,
		State: &config.RepoState{
			IsRunning:            false,
			LastAttemptStartTime: 0,
			LastAttemptRunTime:   0,
			LastAttemptExit:      config.NOT_RUN_YET,
		},
	}
	config.Repos = nil
	config.Repos = append(config.Repos, &repo)
	config.RepoMap = make(map[string]*config.Repo)
	config.RepoMap["nux"] = &repo

	r, w := net.Pipe()
	go func() {
		if !ForceSync(w, "nux") {
			t.Errorf("Sync for nux did not start")
		}
		w.Close()
	}()
	msg, err := ioutil.ReadAll(r)
	r.Close()
	if err != nil {
		t.Errorf(err.Error())
	}
	expected := "Forced sync for nux"
	if expected != string(msg) {
		t.Errorf("Expected:\n" + expected + "\nGot:\n" + string(msg))
	}

	select {
	case result := <-doneChan:
		if result.Exit != config.SUCCESS {
			t.Errorf("Sync should exit with SUCCESS, got %d", result.Exit)
		}
	case <-time.After(3 * time.Second):
		t.Errorf("Dummy sync should be done in 1 second, waited 3 seconds")
	}

	// Part 2: attempt the same thing but with repo.State.IsRunning = true
	r, w = net.Pipe()
	go func() {
		if ForceSync(w, "nux") {
			t.Errorf("Sync for nux should not have started")
		}
		w.Close()
	}()
	msg, err = ioutil.ReadAll(r)
	r.Close()
	if err != nil {
		t.Errorf(err.Error())
	}
	expected = "Could not force sync: nux is already syncing."
	if expected != string(msg) {
		t.Errorf("Expected:\n" + expected + "\nGot:\n" + string(msg))
	}

	select {
	case <-doneChan:
		t.Errorf("Sync for nux should not have been started")
	case <-time.After(2 * time.Second):
	}

	// Part 3: attempt a force sync with a repo that does not exist
	r, w = net.Pipe()
	go func() {
		if ForceSync(w, "nixx") {
			t.Errorf("Sync for nixx should not have started")
		}
		w.Close()
	}()
	msg, err = ioutil.ReadAll(r)
	r.Close()
	if err != nil {
		t.Errorf(err.Error())
	}
	expected = "nixx is not tracked so cannot sync"
	if expected != string(msg) {
		t.Errorf("Expected:\n" + expected + "\nGot:\n" + string(msg))
	}
}

// Test that StartListener stops properly when told to and that it properly creates a unix socket
func TestStartListener(t *testing.T) {
	saveConf := config.Conf
	connChan := make(chan net.Conn)
	stopLisChan := make(chan struct{})
	wait := make(chan struct{})
	defer func() {
		config.Conf = saveConf
		close(connChan)
		close(stopLisChan)
	}()
	config.Conf = config.Config{
		SockPath: "/tmp/merlin_listener_test.sock",
	}

	// Test 1: check that closing/sending something to stopLisChan will stop the listener
	// 		   and that a new listener can be created after stopping the old one
	go func() {
		StartListener(connChan, stopLisChan)
		wait <- struct{}{}
	}()
	stopLisChan <- struct{}{}
	select {
	case <-wait:
	case <-time.After(3 * time.Second):
		t.Errorf("StartListener should stop when struct{}{} is sent to stopLisChan")
	}

	go func() {
		StartListener(connChan, stopLisChan)
		wait <- struct{}{}
	}()
	close(stopLisChan)
	select {
	case <-wait:
	case <-time.After(3 * time.Second):
		t.Errorf("StartListener should stop when stopLisChan is closed")
	}
	close(wait)

	// Test 2: check that connections can be made to the unix socket
	// 		   this test does not appear to be very stable (I think there is a race condition somewhere)
	stopLisChan = make(chan struct{})
	go StartListener(connChan, stopLisChan)
	waitForMsg := func(expected string) {
		select {
		case conn := <-connChan:
			msg, err := ioutil.ReadAll(conn)
			if err != nil {
				t.Errorf(err.Error())
			} else if expected != string(msg) {
				t.Errorf("Message expected was " + expected + " got " + string(msg))
			}
			conn.Close()
		case <-time.After(3 * time.Second):
			t.Errorf("StartListener should stop when struct{}{} is sent to stopLisChan")
		}
	}
	sendMsg := func(msg string) {
		<-time.After(500 * time.Millisecond)
		send, err := net.Dial("unix", "/tmp/merlin_listener_test.sock")
		if err != nil {
			panic(err)
		}
		_, err = send.Write([]byte(msg))
		if err != nil {
			t.Errorf(err.Error())
		}
		send.Close()
	}
	go func() {
		waitForMsg("status")
	}()
	sendMsg("status")

	go func() {
		waitForMsg("sync:uuunix")
	}()
	sendMsg("sync:uuunix")

	go func() {
		waitForMsg("$UPz2L2yWsE^UY8iG9JX@^dBb@5yb*")
	}()
	sendMsg("$UPz2L2yWsE^UY8iG9JX@^dBb@5yb*")

	os.Remove("/tmp/merlin_listener_test.sock")
}
