package caddyhugo

import (
	"encoding/json"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"sync"
	"testing"
	"time"

	"git.stephensearles.com/stephen/acedoc"
)

type World struct {
	CH         *CaddyHugo
	BlogFolder string
}

func (w *World) Clean() {
	if w.BlogFolder != "" {
		os.RemoveAll(w.BlogFolder)
	}
}

func NewWorld(t *testing.T) *World {
	dir, err := ioutil.TempDir("", "caddy-hugo2-test-")
	if err != nil {
		t.Fatalf("error initializing test environment: %v", err)
	}

	w := &World{BlogFolder: dir}

	cmd := exec.Command("hugo", "new", "site", dir)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("error initializing test site: %v\n\n%v", err, string(out))
	}

	w.CH = &CaddyHugo{}
	w.CH.Setup(dir)

	return w
}

func TestEdits(t *testing.T) {
	w := NewWorld(t)
	defer w.Clean()

	const title = "sometitle"
	var contentPath = path.Join("content", title+".md")

	w.CH.NewContent(title, "")

	send := []acedoc.Delta{
		acedoc.Insert(0, 0, "hello"),
		acedoc.Insert(0, 5, " world"),
		acedoc.Insert(0, 11, " world"),
	}
	var mtx sync.Mutex
	received := []acedoc.Delta{}

	doc, err := w.CH.editSession(contentPath)
	if err != nil {
		t.Fatal("error creating document client:", err)
	}

	doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error {
		// receive some deltas...
		mtx.Lock()
		defer mtx.Unlock()
		received = append(received, ds...)
		return nil
	}))

	_, ok := w.CH.hasEditSession(contentPath)
	if !ok {
		t.Fatal("expected there to be an established client")
	}

	doc.doc.Apply(send...)

	<-time.After(5 * time.Second)

	mtx.Lock()
	defer mtx.Unlock()
	if len(received) != len(send) {
		t.Errorf("expected %d deltas, received %d; expected: %v, received: %v", len(send), len(received), send, received)
	}

}

type WebsocketTester struct {
	receivedPointer int
	received        [][]byte
	wroteMessages   []Message
	wroteDeltas     []acedoc.Delta
	mtx             sync.Mutex
}

func (ws *WebsocketTester) ReadJSON(v interface{}) error {
	ws.mtx.Lock()
	defer ws.mtx.Unlock()

	if len(ws.received) <= ws.receivedPointer {
		return nil
	}

	err := json.Unmarshal(ws.received[ws.receivedPointer], v)
	ws.receivedPointer++
	return err
}

func (ws *WebsocketTester) WriteJSON(v interface{}) error {
	ws.mtx.Lock()
	defer ws.mtx.Unlock()

	m, ok := v.(Message)
	if !ok {
		panic("wrong type written to WebsocketTester")
	}

	if len(m.Deltas) == 0 {
		return nil
	}

	ws.wroteMessages = append(ws.wroteMessages, m)
	ws.wroteDeltas = append(ws.wroteDeltas, m.Deltas...)

	return nil
}

func (ws *WebsocketTester) ReceiveJSON(v interface{}) error {
	ws.mtx.Lock()
	defer ws.mtx.Unlock()

	out, err := json.Marshal(v)
	if err != nil {
		return err
	}

	ws.received = append(ws.received, out)
	return nil
}

func TestDeltasSingle(t *testing.T) {
	w := NewWorld(t)
	defer w.Clean()

	const title = "test"

	_, err := w.CH.NewContent(title, "")
	if err != nil {
		t.Fatal("couldn't create new content:", err)
	}

	client := new(WebsocketTester)

	doc, err := w.CH.editSession("content/" + title + ".md")
	if err != nil {
		t.Fatal("couldn't establish docref for client 0:", err)
	}

	go w.CH.handleDeltaConn(client, doc)

	a := acedoc.Insert(0, 0, "a")

	// pretend to get one sent from the "browser"
	client.ReceiveJSON(w.CH.Message(a))

	// wait to make sure it was processed
	time.Sleep(50 * time.Millisecond)

	// we shouldn't have written back to the client,
	// so we expect to have written 0 messages
	if len(client.wroteMessages) != 0 {
		t.Errorf("client wrote %d messages, should have written %d", len(client.wroteMessages), 0)
	}

	// we received one, so make sure that's counted properly
	if len(client.received) != 1 {
		t.Errorf("client has %d messages, should have received %d", len(client.received), 1)
	}
}

func TestDeltasDouble(t *testing.T) {
	w := NewWorld(t)
	defer w.Clean()

	const title = "test"

	_, err := w.CH.NewContent(title, "")
	if err != nil {
		t.Fatal("couldn't create new content:", err)
	}

	clientA := new(WebsocketTester)
	clientB := new(WebsocketTester)

	doc, err := w.CH.editSession("content/" + title + ".md")
	if err != nil {
		t.Fatal("couldn't establish docref for client 0:", err)
	}

	go w.CH.handleDeltaConn(clientA, doc)
	go w.CH.handleDeltaConn(clientB, doc)

	// send the first message, simulating the browser on clientA
	clientA.ReceiveJSON(w.CH.Message(acedoc.Insert(0, 0, "a")))

	time.Sleep(100 * time.Millisecond)

	clientA.mtx.Lock()
	clientB.mtx.Lock()

	// so we expect clientA to have written 0 messages, and
	// clientB to have written 1
	if len(clientA.wroteMessages) != 0 || len(clientB.wroteMessages) != 1 {
		t.Errorf("clientA wrote %d messages, should have written 0. clientB wrote %d, should have written 1", len(clientA.wroteMessages), len(clientB.wroteMessages))
	}

	// we received one via clientA and zero via clientB, so make sure
	// that's counted properly
	if len(clientA.received) != 1 || len(clientB.received) != 0 {
		t.Errorf("clientA has %d messages, should have received 1; clientB has %d messages, should have received 0", len(clientA.received), len(clientB.received))
	}

	clientA.mtx.Unlock()
	clientB.mtx.Unlock()

	// send the second message, via clientB
	clientB.ReceiveJSON(w.CH.Message(acedoc.Insert(0, 0, "b")))

	time.Sleep(400 * time.Millisecond)

	clientA.mtx.Lock()
	clientB.mtx.Lock()

	// so we expect clientA to have written 1 message this time, and
	// clientB to have written nothing new, so 1 still
	if len(clientA.wroteMessages) != 1 || len(clientB.wroteMessages) != 1 {
		t.Errorf("clientA wrote %d messages, should have written 1. clientB wrote %d, should have written 1 (just from before)", len(clientA.wroteMessages), len(clientB.wroteMessages))
	}

	// we received zero (new) via clientA and one via clientB, so make sure
	// that's counted properly
	if len(clientA.received) != 1 || len(clientB.received) != 1 {
		t.Errorf("clientA has %d messages, should have received 1; clientB has %d messages, should have received 1", len(clientA.received), len(clientB.received))
	}
	clientA.mtx.Unlock()
	clientB.mtx.Unlock()
}

func TestDeltasMulti(t *testing.T) {
	w := NewWorld(t)
	defer w.Clean()

	const title = "test"

	_, err := w.CH.NewContent(title, "")
	if err != nil {
		t.Fatal("couldn't create new content:", err)
	}

	clients := []*WebsocketTester{{}, {}, {}}

	doc, err := w.CH.editSession("content/" + title + ".md")
	if err != nil {
		t.Fatal("couldn't establish docref:", err)
	}

	go w.CH.handleDeltaConn(clients[0], doc)
	go w.CH.handleDeltaConn(clients[1], doc)
	go w.CH.handleDeltaConn(clients[2], doc)

	a := acedoc.Insert(0, 0, "a")
	b := acedoc.Insert(0, 0, "b")
	c := acedoc.Insert(0, 0, "c")

	clients[0].ReceiveJSON(w.CH.Message(a))
	clients[1].ReceiveJSON(w.CH.Message(b))
	clients[2].ReceiveJSON(w.CH.Message(c))

	time.Sleep(400 * time.Millisecond)

	for i, client := range clients {
		client.mtx.Lock()
		// all clients should have "written" 2 deltas (could be the same
		// message) that came from the other clients
		if len(client.wroteDeltas) != 2 {
			t.Errorf("client %d wrote %d deltas, should have written 2", i, len(client.wroteDeltas))
		}

		// all clients "received" 1 message from the "browser"
		if len(client.received) != 1 {
			t.Errorf("client %d has %d messages, should have received 1", i, len(client.received))
		}
		client.mtx.Unlock()
	}
}

func TestPagesInPagesOut(t *testing.T) {
	w := NewWorld(t)
	defer w.Clean()

	// check there's no content at first
	c, err := GetContent(w.BlogFolder, w.CH.HugoSites)
	if err != nil {
		t.Fatalf("couldn't get content from a blank test environment: %v", err)
	}
	if len(c) != 0 {
		t.Fatalf("expected a blank test environment, but saw %d pages", len(c))
	}

	titles := []string{
		"test1",
		"TEST 2!!",
	}

	found := map[string]bool{}

	// create some known content
	for i, title := range titles {
		w.CH.NewContent(title, "")
		c, err = GetContent(w.BlogFolder, w.CH.HugoSites)
		if err != nil {
			t.Fatalf("couldn't get content from the test environment: %v", err)
		}
		if len(c)-1 != i {
			t.Fatalf("expected %d page, but saw %d pages", i+1, len(c))
		}

	}

	// make sure we get the content out that we just created
	c, err = GetContent(w.BlogFolder, w.CH.HugoSites)
	if err != nil {
		t.Fatalf("couldn't get content from the test environment: %v", err)
	}

	for _, content := range c {
		if content.Metadata == nil {
			t.Errorf("didn't see metadata for %q", content.Filename)
		}
		found[content.Filename] = true
	}

	var missingSomething bool
	for _, title := range titles {
		adjusted := path.Join("content", docname(title)+".md")
		if !found[adjusted] {
			missingSomething = true
			t.Errorf("expected to find title %q, but didn't see it", adjusted)
		}
	}

	if missingSomething {
		t.Logf("found titles: %v", found)
	}
}
