package npipe

import (
	"bufio"
	"crypto/rand"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/rpc"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"
)

const (
	clientMsg    = "Hi server!\n"
	serverMsg    = "Hi there, client!\n"
	fileTemplate = "62DA0493-99A1-4327-B5A8-6C4E4466C3FC.txt"
)

// TestBadDial tests that if you dial something other than a valid pipe path, that you get back a
// PipeError and that you don't accidently create a file on disk (since dial uses OpenFile)
func TestBadDial(t *testing.T) {
	fn := filepath.Join("C:\\", fileTemplate)
	ns := []string{fn, "http://www.google.com", "somethingbadhere"}
	for _, n := range ns {
		c, err := Dial(n)
		if _, ok := err.(PipeError); !ok {
			t.Errorf("Dialing '%s' did not result in correct error! Expected PipeError, got '%v'",
				n, err)
		}
		if c != nil {
			t.Errorf("Dialing '%s' returned non-nil connection", n)
		}
		if b, _ := exists(n); b {
			t.Errorf("Dialing '%s' incorrectly created file on disk", n)
		}
	}
}

// TestDialExistingFile tests that if you dial with the name of an existing file,
// that you don't accidentally open the file (since dial uses OpenFile)
func TestDialExistingFile(t *testing.T) {
	tempdir := os.TempDir()
	fn := filepath.Join(tempdir, fileTemplate)
	if f, err := os.Create(fn); err != nil {
		t.Fatalf("Unexpected error creating file '%s': '%v'", fn, err)
	} else {
		// we don't actually need to write to the file, just need it to exist
		f.Close()
		defer os.Remove(fn)
	}
	c, err := Dial(fn)
	if _, ok := err.(PipeError); !ok {
		t.Errorf("Dialing '%s' did not result in error! Expected PipeError, got '%v'", fn, err)
	}
	if c != nil {
		t.Errorf("Dialing '%s' returned non-nil connection", fn)
	}
}

// TestBadListen tests that if you listen on a bad address, that we get back a PipeError
func TestBadListen(t *testing.T) {
	addrs := []string{"not a valid pipe address", `\\127.0.0.1\pipe\TestBadListen`}
	for _, address := range addrs {
		ln, err := Listen(address)
		if _, ok := err.(PipeError); !ok {
			t.Errorf("Listening on '%s' did not result in correct error! Expected PipeError, got '%v'",
				address, err)
		}
		if ln != nil {
			t.Errorf("Listening on '%s' returned non-nil listener.", address)
		}
	}
}

// TestDoubleListen makes sure we can't listen to the same address twice.
func TestDoubleListen(t *testing.T) {
	address := `\\.\pipe\TestDoubleListen`
	ln1, err := Listen(address)
	if err != nil {
		t.Fatalf("Listen(%q): %v", address, err)
	}
	defer ln1.Close()

	ln2, err := Listen(address)
	if err == nil {
		ln2.Close()
		t.Fatalf("second Listen on %q succeeded.", address)
	}
}

// TestPipeConnected tests whether we correctly handle clients connecting
// and then closing the connection between creating and connecting the
// pipe on the server side.
func TestPipeConnected(t *testing.T) {
	address := `\\.\pipe\TestPipeConnected`
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Listen(%q): %v", address, err)
	}
	defer ln.Close()

	// Create a client connection and close it immediately.
	clientConn, err := Dial(address)
	if err != nil {
		t.Fatalf("Error from dial: %v", err)
	}
	clientConn.Close()

	content := "test"
	go func() {
		// Now create a real connection and send some data.
		clientConn, err := Dial(address)
		if err != nil {
			t.Fatalf("Error from dial: %v", err)
		}
		if _, err := clientConn.Write([]byte(content)); err != nil {
			t.Fatalf("Error writing to pipe: %v", err)
		}
		clientConn.Close()
	}()

	serverConn, err := ln.Accept()
	if err != nil {
		t.Fatalf("Error from accept: %v", err)
	}
	result, err := ioutil.ReadAll(serverConn)
	if err != nil {
		t.Fatalf("Error from ReadAll: %v", err)
	}
	if string(result) != content {
		t.Fatalf("Got %s, expected: %s", string(result), content)
	}
	serverConn.Close()
}

// TestListenCloseListen tests whether Close() actually closes a named pipe properly.
func TestListenCloseListen(t *testing.T) {
	address := `\\.\pipe\TestListenCloseListen`
	ln1, err := Listen(address)
	if err != nil {
		t.Fatalf("Listen(%q): %v", address, err)
	}
	ln1.Close()

	ln2, err := Listen(address)
	if err != nil {
		t.Fatalf("second Listen on %q failed.", address)
	}
	ln2.Close()
}

// TestCloseFileHandles tests that all PipeListener handles are actualy closed after
// calling Close()
func TestCloseFileHandles(t *testing.T) {
	address := `\\.\pipe\TestCloseFileHandles`
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Error listening on %q: %v", address, err)
	}
	defer ln.Close()
	server := rpc.NewServer()
	service := &RPCService{}
	server.Register(service)
	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				// Ignore errors produced by a closed listener.
				if err != ErrClosed {
					t.Errorf("ln.Accept(): %v", err.Error())
				}
				break
			}
			go server.ServeConn(conn)
		}
	}()
	conn, err := Dial(address)
	if err != nil {
		t.Fatalf("Error dialing %q: %v", address, err)
	}
	client := rpc.NewClient(conn)
	defer client.Close()
	req := "dummy"
	resp := ""
	if err = client.Call("RPCService.GetResponse", req, &resp); err != nil {
		t.Fatalf("Error calling RPCService.GetResponse: %v", err)
	}
	if req != resp {
		t.Fatalf("Unexpected result (expected: %q, got: %q)", req, resp)
	}
	ln.Close()

	if ln.acceptHandle != 0 {
		t.Fatalf("Failed to close acceptHandle")
	}
	if ln.acceptOverlapped.HEvent != 0 {
		t.Fatalf("Failed to close acceptOverlapped handle")
	}
}

// TestCancelListen tests whether Accept() can be cancelled by closing the listener.
func TestCancelAccept(t *testing.T) {
	address := `\\.\pipe\TestCancelListener`
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Listen(%q): %v", address, err)
	}

	cancelled := make(chan struct{})
	started := make(chan struct{})
	go func() {
		close(started)
		conn, _ := ln.Accept()
		if conn != nil {
			t.Fatalf("Unexpected incoming connection: %v", conn)
			conn.Close()
		}
		cancelled <- struct{}{}
	}()
	<-started
	// Close listener after 20ms. This should give the go routine enough time to be actually
	// waiting for incoming connections inside ln.Accept().
	time.AfterFunc(20*time.Millisecond, func() {
		if err := ln.Close(); err != nil {
			t.Fatalf("Error closing listener: %v", err)
		}
	})
	// Any Close() should abort the ln.Accept() call within 100ms.
	// We fail with a timeout otherwise, to avoid blocking forever on a failing test.
	timeout := time.After(100 * time.Millisecond)
	select {
	case <-cancelled:
		// This is what should happen.
	case <-timeout:
		t.Fatal("Timeout trying to cancel accept.")
	}
}

// Test that PipeConn's read deadline works correctly
func TestReadDeadline(t *testing.T) {
	address := `\\.\pipe\TestReadDeadline`
	var wg sync.WaitGroup
	wg.Add(1)

	go listenAndWait(address, wg, t)
	defer wg.Done()

	c, err := Dial(address)
	if err != nil {
		t.Fatalf("Error dialing into pipe: %v", err)
	}
	if c == nil {
		t.Fatal("Unexpected nil connection from Dial")
	}
	defer c.Close()
	deadline := time.Now().Add(time.Millisecond * 50)
	c.SetReadDeadline(deadline)
	msg, err := bufio.NewReader(c).ReadString('\n')
	end := time.Now()
	if msg != "" {
		t.Errorf("Pipe read timeout returned a non-empty message: %s", msg)
	}
	if err == nil {
		t.Error("Pipe read timeout returned nil error")
	} else {
		pe, ok := err.(PipeError)
		if !ok {
			t.Errorf("Got wrong error returned, expected PipeError, got '%t'", err)
		}
		if !pe.Timeout() {
			t.Error("Pipe read timeout didn't return an error indicating the timeout")
		}
	}
	checkDeadline(deadline, end, t)
}

// listenAndWait simply sets up a pipe listener that does nothing and closes after the waitgroup
// is done.
func listenAndWait(address string, wg sync.WaitGroup, t *testing.T) {
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Error starting to listen on pipe: %v", err)
	}
	if ln == nil {
		t.Fatal("Got unexpected nil listener")
	}
	conn, err := ln.Accept()
	if err != nil {
		t.Fatalf("Error accepting connection: %v", err)
	}
	if conn == nil {
		t.Fatal("Got unexpected nil connection")
	}
	defer conn.Close()
	// don't read or write anything
	wg.Wait()
}

// TestWriteDeadline tests that PipeConn's write deadline works correctly
func TestWriteDeadline(t *testing.T) {
	address := `\\.\pipe\TestWriteDeadline`
	var wg sync.WaitGroup
	wg.Add(1)

	go listenAndWait(address, wg, t)
	defer wg.Done()
	c, err := Dial(address)
	if err != nil {
		t.Fatalf("Error dialing into pipe: %v", err)
	}
	if c == nil {
		t.Fatal("Unexpected nil connection from Dial")
	}

	// windows pipes have a buffer, so even if we don't read from the pipe,
	// the write may succeed anyway, so we have to write a whole bunch to
	// test the time out
	deadline := time.Now().Add(time.Millisecond * 50)
	c.SetWriteDeadline(deadline)
	buffer := make([]byte, 1<<16)
	if _, err = io.ReadFull(rand.Reader, buffer); err != nil {
		t.Fatalf("Couldn't generate random buffer: %v", err)
	}
	_, err = c.Write(buffer)

	end := time.Now()

	if err == nil {
		t.Error("Pipe write timeout returned nil error")
	} else {
		pe, ok := err.(PipeError)
		if !ok {
			t.Errorf("Got wrong error returned, expected PipeError, got '%t'", err)
		}
		if !pe.Timeout() {
			t.Error("Pipe write timeout didn't return an error indicating the timeout")
		}
	}
	checkDeadline(deadline, end, t)
}

// TestDialTimeout tests that the DialTimeout function will actually timeout correctly
func TestDialTimeout(t *testing.T) {
	timeout := time.Millisecond * 150
	deadline := time.Now().Add(timeout)
	c, err := DialTimeout(`\\.\pipe\TestDialTimeout`, timeout)
	end := time.Now()
	if c != nil {
		t.Errorf("DialTimeout returned non-nil connection: %v", c)
	}
	if err == nil {
		t.Error("DialTimeout returned nil error after timeout")
	} else {
		pe, ok := err.(PipeError)
		if !ok {
			t.Errorf("Got wrong error returned, expected PipeError, got '%t'", err)
		}
		if !pe.Timeout() {
			t.Error("Dial timeout didn't return an error indicating the timeout")
		}
	}
	checkDeadline(deadline, end, t)
}

// TestDialNoTimeout tests that the DialTimeout function will properly wait for the pipe and
// connect when it is available
func TestDialNoTimeout(t *testing.T) {
	timeout := time.Millisecond * 500
	address := `\\.\pipe\TestDialNoTimeout`
	go func() {
		<-time.After(50 * time.Millisecond)
		listenAndClose(address, t)
	}()

	deadline := time.Now().Add(timeout)
	c, err := DialTimeout(address, timeout)
	end := time.Now()

	if c == nil {
		t.Error("DialTimeout returned unexpected nil connection")
	}
	if err != nil {
		t.Error("DialTimeout returned unexpected non-nil error: ", err)
	}
	if end.After(deadline) {
		t.Fatalf("Ended %v after deadline", end.Sub(deadline))
	}
}

// TestDial tests that you can dial before a pipe is available,
// and that it'll pick up the pipe once it's ready
func TestDial(t *testing.T) {
	address := `\\.\pipe\TestDial`
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Done()
		conn, err := Dial(address)
		if err != nil {
			t.Fatalf("Got unexpected error from Dial: %v", err)
		}
		if conn == nil {
			t.Fatal("Got unexpected nil connection from Dial")
		}
		if err := conn.Close(); err != nil {
			t.Fatalf("Got unexpected error from conection.Close(): %v", err)
		}
	}()

	wg.Wait()
	<-time.After(50 * time.Millisecond)
	listenAndClose(address, t)
}

type RPCService struct{}

func (s *RPCService) GetResponse(request string, response *string) error {
	*response = request
	return nil
}

// TestGoRPC tests that you can run go RPC over the pipe,
// and that overlapping bi-directional communication is working
// (write while a blocking read is in progress).
func TestGoRPC(t *testing.T) {
	address := `\\.\pipe\TestRPC`
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Error listening on %q: %v", address, err)
	}
	waitExit := make(chan struct{})
	defer func() {
		ln.Close()
		<-waitExit
	}()

	go func() {
		server := rpc.NewServer()
		server.Register(&RPCService{})
		for {
			conn, err := ln.Accept()
			if err != nil {
				// Ignore errors produced by a closed listener.
				if err != ErrClosed {
					t.Errorf("ln.Accept(): %v", err.Error())
				}
				break
			}
			go server.ServeConn(conn)
		}
		close(waitExit)
	}()
	conn, err := Dial(address)
	if err != nil {
		t.Fatalf("Error dialing %q: %v", address, err)
	}
	client := rpc.NewClient(conn)
	defer client.Close()
	req := "dummy"
	var resp string
	if err = client.Call("RPCService.GetResponse", req, &resp); err != nil {
		t.Fatalf("Error calling RPCService.GetResponse: %v", err)
	}
	if req != resp {
		t.Fatalf("Unexpected result (expected: %q, got: %q)", req, resp)
	}
}

// listenAndClose is a helper method to just listen on a pipe and close as soon as someone connects.
func listenAndClose(address string, t *testing.T) {
	ln, err := Listen(address)
	if err != nil {
		t.Fatalf("Got unexpected error from Listen: %v", err)
	}
	if ln == nil {
		t.Fatal("Got unexpected nil listener from Listen")
	}
	conn, err := ln.Accept()
	if err != nil {
		t.Fatalf("Got unexpected error from Accept: %v", err)
	}
	if conn == nil {
		t.Fatal("Got unexpected nil connection from Accept")
	}
	if err := conn.Close(); err != nil {
		t.Fatalf("Got unexpected error from conection.Close(): %v", err)
	}
}

// TestCommonUseCase is a full run-through of the most common use case, where you create a listener
// and then dial into it with several clients in succession
func TestCommonUseCase(t *testing.T) {
	addrs := []string{`\\.\pipe\TestCommonUseCase`, `\\127.0.0.1\pipe\TestCommonUseCase`}
	// always listen on the . version, since IP won't work for listening
	ln, err := Listen(addrs[0])
	if err != nil {
		t.Fatalf("Listen(%q) failed: %v", addrs[0], err)
	}
	defer ln.Close()

	for _, address := range addrs {
		convos := 5
		clients := 10

		wg := sync.WaitGroup{}

		for x := 0; x < clients; x++ {
			wg.Add(1)
			go startClient(address, &wg, convos, t)
		}

		go startServer(ln, convos, t)

		select {
		case <-wait(&wg):
		// good!
		case <-time.After(time.Second):
			t.Fatal("Failed to finish after a reasonable timeout")
		}
	}
}

// wait simply waits on the waitgroup and closes the returned channel when done.
func wait(wg *sync.WaitGroup) <-chan struct{} {
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()
	return done
}

// startServer accepts connections and spawns goroutines to handle them
func startServer(ln *PipeListener, iter int, t *testing.T) {
	for {
		conn, err := ln.Accept()
		if err == ErrClosed {
			return
		}
		if err != nil {
			t.Fatalf("Error accepting connection: %v", err)
		}
		go handleConnection(conn, iter, t)
	}
}

// handleConnection is the goroutine that handles connections on the server side
// it expects to read a message and then write a message, convos times, before exiting.
func handleConnection(conn net.Conn, convos int, t *testing.T) {
	r := bufio.NewReader(conn)
	for x := 0; x < convos; x++ {
		msg, err := r.ReadString('\n')
		if err != nil {
			t.Fatalf("Error reading from server connection: %v", err)
		}
		if msg != clientMsg {
			t.Fatalf("Read incorrect message from client. Expected '%s', got '%s'", clientMsg, msg)
		}

		if _, err := fmt.Fprint(conn, serverMsg); err != nil {
			t.Fatalf("Error on server writing to pipe: %v", err)
		}
	}
	if err := conn.Close(); err != nil {
		t.Fatalf("Error closing server side of connection: %v", err)
	}
}

// startClient waits on a pipe at the given address. It expects to write a message and then
// read a message from the pipe, convos times, and then sends a message on the done
// channel
func startClient(address string, wg *sync.WaitGroup, convos int, t *testing.T) {
	defer wg.Done()
	c := make(chan *PipeConn)
	go asyncdial(address, c, t)

	var conn *PipeConn
	select {
	case conn = <-c:
	case <-time.After(time.Second):
		// Yes this is a long timeout, but sometimes it really does take a long time.
		t.Fatalf("Client timed out waiting for dial to resolve")
	}
	r := bufio.NewReader(conn)
	for x := 0; x < convos; x++ {
		if _, err := fmt.Fprint(conn, clientMsg); err != nil {
			t.Fatalf("Error on client writing to pipe: %v", err)
		}

		msg, err := r.ReadString('\n')
		if err != nil {
			t.Fatalf("Error reading from client connection: %v", err)
		}
		if msg != serverMsg {
			t.Fatalf("Read incorrect message from server. Expected '%s', got '%s'", serverMsg, msg)
		}
	}

	if err := conn.Close(); err != nil {
		t.Fatalf("Error closing client side of pipe %v", err)
	}
}

// asyncdial is a helper that dials and returns the connection on the given channel.
// this is useful for being able to give dial a timeout
func asyncdial(address string, c chan *PipeConn, t *testing.T) {
	conn, err := Dial(address)
	if err != nil {
		t.Fatalf("Error from dial: %v", err)
	}
	c <- conn
}

// exists is a simple helper function to detect if a file exists on disk
func exists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

func checkDeadline(deadline, end time.Time, t *testing.T) {
	if end.Before(deadline) {
		t.Fatalf("Ended %v before deadline", deadline.Sub(end))
	}
	diff := end.Sub(deadline)

	// we need a huge fudge factor here because Windows has really poor
	// resolution for timeouts, and in practice, the timeout can be 400ms or
	// more after the expected timeout.
	if diff > 500*time.Millisecond {
		t.Fatalf("Ended significantly (%v) after deadline", diff)
	}
}
