///////////////////////////////////////////////////////////////////////////////
// Copyright © 2020 xx network SEZC                                          //
//                                                                           //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file                                                              //
///////////////////////////////////////////////////////////////////////////////

package stoppable

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

// Tests that NewMulti returns a Multi that is running with the given name.
func TestNewMulti(t *testing.T) {
	name := "testMulti"
	multi := NewMulti(name)

	if multi.name != name {
		t.Errorf("NewMulti returned Multi with incorrect name."+
			"\nexpected: %s\nreceived: %s", name, multi.name)
	}
}

// Tests that Multi.Add adds all the stoppables to the list.
func TestMulti_Add(t *testing.T) {
	multi := NewMulti("testMulti")
	expected := []Stoppable{
		NewSingle("testSingle0"),
		NewMulti("testMulti0"),
		NewSingle("testSingle1"),
		NewMulti("testMulti1"),
	}

	for _, stoppable := range expected {
		multi.Add(stoppable)
	}

	if !reflect.DeepEqual(multi.stoppables, expected) {
		t.Errorf("Add did not add the correct Stoppables."+
			"\nexpected: %+v\nreceived: %+v", multi.stoppables, expected)
	}
}

// Unit test of Multi.Name.
func TestMulti_Name(t *testing.T) {
	name := "testMulti"
	multi := NewMulti(name)

	// Add stoppables and created list of their names
	var nameList []string
	for i := 0; i < 10; i++ {
		newName := ""
		if i%2 == 0 {
			newName = "single" + strconv.Itoa(i)
			multi.Add(NewSingle(newName))
		} else {
			newMulti := NewMulti("multi" + strconv.Itoa(i))
			if i != 5 {
				newMulti.Add(NewMulti("multiA"))
				newMulti.Add(NewMulti("multiB"))
			}
			multi.Add(newMulti)
			newName = newMulti.Name()
		}
		nameList = append(nameList, newName)
	}

	expected := name + "{" + strings.Join(nameList, ", ") + "}"

	if multi.Name() != expected {
		t.Errorf("Name failed to return the expected string."+
			"\nexpected: %s\nreceived: %s", expected, multi.Name())
	}
}

// Tests that Multi.Name returns the expected string when it has no stoppables.
func TestMulti_Name_NoStoppables(t *testing.T) {
	name := "testMulti"
	multi := NewMulti(name)

	expected := name + "{}"

	if multi.Name() != expected {
		t.Errorf("Name failed to return the expected string."+
			"\nexpected: %s\nreceived: %s", expected, multi.Name())
	}
}

// Tests that Multi.GetStatus returns the expected Status.
func TestMulti_GetStatus(t *testing.T) {
	multi := NewMulti("testMulti")
	single1 := NewSingle("testSingle1")
	single2 := NewSingle("testSingle2")
	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped))
	multi.Add(single1)
	multi.Add(single2)

	status := multi.GetStatus()
	if status != Running {
		t.Errorf("GetStatus returned the wrong status."+
			"\nexpected: %s\nreceived: %s", Running, status)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping))
	status = multi.GetStatus()
	if status != Stopping {
		t.Errorf("GetStatus returned the wrong status."+
			"\nexpected: %s\nreceived: %s", Stopping, status)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped))
	status = multi.GetStatus()
	if status != Stopped {
		t.Errorf("GetStatus returned the wrong status."+
			"\nexpected: %s\nreceived: %s", Stopped, status)
	}
}

// Tests that Multi.GetStatus returns the expected Status when it has no
// children.
func TestMulti_GetStatus_NoChildren(t *testing.T) {
	multi := NewMulti("testMulti")

	status := multi.GetStatus()
	if status != Stopped {
		t.Errorf("GetStatus returned the wrong status."+
			"\nexpected: %s\nreceived: %s", Stopped, status)
	}
}

// Tests that Multi.IsRunning returns the expected value when the Multi is
// marked as running, stopping, and stopped.
func TestMulti_IsRunning(t *testing.T) {
	multi := NewMulti("testMulti")
	single1 := NewSingle("testSingle1")
	single2 := NewSingle("testSingle2")
	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopping))
	multi.Add(single1)
	multi.Add(single2)

	if result := multi.IsRunning(); !result {
		t.Errorf("IsRunning returned the wrong value when running."+
			"\nexpected: %t\nreceived: %t", true, result)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping))
	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped))
	if result := multi.IsRunning(); result {
		t.Errorf("IsRunning returned the wrong value when stopping."+
			"\nexpected: %t\nreceived: %t", false, result)
	}

	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped))
	if result := multi.IsRunning(); result {
		t.Errorf("IsRunning returned the wrong value when stopped."+
			"\nexpected: %t\nreceived: %t", false, result)
	}
}

// Tests that Multi.IsStopping returns the expected value when the Multi is
// marked as running, stopping, and stopped.
func TestMulti_IsStopping(t *testing.T) {
	multi := NewMulti("testMulti")
	single1 := NewSingle("testSingle1")
	single2 := NewSingle("testSingle2")
	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped))
	multi.Add(single1)
	multi.Add(single2)

	if result := multi.IsStopping(); result {
		t.Errorf("IsStopping returned the wrong value when running."+
			"\nexpected: %t\nreceived: %t", true, result)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping))
	if result := multi.IsStopping(); !result {
		t.Errorf("IsStopping returned the wrong value when stopping."+
			"\nexpected: %t\nreceived: %t", false, result)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped))
	if result := multi.IsStopping(); result {
		t.Errorf("IsStopping returned the wrong value when stopped."+
			"\nexpected: %t\nreceived: %t", false, result)
	}
}

// Tests that Multi.IsStopped returns the expected value when the Multi is
// marked as running, stopping, and stopped.
func TestMulti_IsStopped(t *testing.T) {
	multi := NewMulti("testMulti")
	single1 := NewSingle("testSingle1")
	single2 := NewSingle("testSingle2")
	atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped))
	multi.Add(single1)
	multi.Add(single2)

	if result := multi.IsStopped(); result {
		t.Errorf("IsStopped returned the wrong value when running."+
			"\nexpected: %t\nreceived: %t", true, result)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping))
	if result := multi.IsStopped(); result {
		t.Errorf("IsStopped returned the wrong value when stopping."+
			"\nexpected: %t\nreceived: %t", false, result)
	}

	atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped))
	if result := multi.IsStopped(); !result {
		t.Errorf("IsStopped returned the wrong value when stopped."+
			"\nexpected: %t\nreceived: %t", false, result)
	}
}

// Tests that Multi.IsStopped returns true when all of the child stoppables are
// stopped.
func TestMulti_IsStopped_StoppedStatus(t *testing.T) {
	multi := NewMulti("testMulti")
	singles := []*Single{
		NewSingle("testSingle0"),
		NewSingle("testSingle1"),
		NewSingle("testSingle2"),
		NewSingle("testSingle3"),
		NewSingle("testSingle4"),
	}
	for _, single := range singles[:3] {
		atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped))
		multi.Add(single)
	}
	subMulti := NewMulti("subMulti")
	for _, single := range singles[3:] {
		atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped))
		subMulti.Add(single)
	}
	multi.Add(subMulti)

	if !multi.IsStopped() {
		t.Error("IsStopped did not find all stoppables as stopped.")
	}
}

// Error path: tests that Multi.IsStopped returns false when not all of the
// child stoppables are stopped.
func TestMulti_IsStopped_NotStoppedError(t *testing.T) {
	multi := NewMulti("testMulti")
	singles := []*Single{
		NewSingle("testSingle0"),
		NewSingle("testSingle1"),
		NewSingle("testSingle2"),
		NewSingle("testSingle3"),
		NewSingle("testSingle4"),
	}
	for _, single := range singles {
		multi.Add(single)
	}

	for _, single := range singles[:4] {
		atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped))
	}

	if multi.IsStopped() {
		t.Error("IsStopped found all the stoppables as stopped when some are " +
			"still running")
	}
}

// Tests that Multi.Close sends on all Single quit channels.
func TestMulti_Close(t *testing.T) {
	multi := NewMulti("testMulti")
	singles := []*Single{
		NewSingle("testSingle0"),
		NewSingle("testSingle1"),
		NewSingle("testSingle2"),
		NewSingle("testSingle3"),
		NewSingle("testSingle4"),
	}
	for _, single := range singles[:3] {
		multi.Add(single)
	}
	subMulti := NewMulti("subMulti")
	for _, single := range singles[3:] {
		subMulti.Add(single)
	}
	multi.Add(subMulti)

	for _, single := range singles {
		go func(single *Single) {
			select {
			case <-time.NewTimer(5 * time.Millisecond).C:
				t.Errorf("Single %s failed to quit.", single.Name())
			case <-single.Quit():
			}
		}(single)
	}

	err := multi.Close()
	if err != nil {
		t.Errorf("Close() returned an error: %v", err)
	}

	err = multi.Close()
	if err != nil {
		t.Errorf("Close() returned an error: %v", err)
	}
}

// Error path: tests that Multi.Close returns the expected error when the Single
// stoppables are not running.
func TestMulti_Close_StoppableCloseError(t *testing.T) {
	multi := NewMulti("testMulti")
	var singles []*Single
	for i := 0; i < 5; i++ {
		single := NewSingle("testSingle" + strconv.Itoa(i))
		singles = append(singles, single)
		multi.Add(single)
		atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped))
	}

	var wg sync.WaitGroup
	for _, single := range singles {
		wg.Add(1)
		go func(single *Single) {
			select {
			case <-time.NewTimer(15 * time.Millisecond).C:
			case <-single.Quit():
				t.Errorf("Single %s to quit when it should have failed.",
					single.Name())
			}
			wg.Done()
		}(single)
	}

	expectedErr := fmt.Sprintf(closeMultiErr, multi.name, 0, 0)
	expectedErr = strings.SplitN(expectedErr, " 0/0", 2)[0]

	err := multi.Close()
	if err == nil || !strings.Contains(err.Error(), expectedErr) {
		t.Errorf("Close() did not return the expected error."+
			"\nexpected: %s\nreceived: %v", expectedErr, err)
	}

	wg.Wait()

	err = multi.Close()
	if err != nil {
		t.Errorf("Close() returned an error: %v", err)
	}
}