// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package ipn

import (
	"bytes"
	"context"
	"encoding/json"
	"testing"
	"time"

	"tailscale.com/tailcfg"
	"tailscale.com/tstest"
)

func TestReadWrite(t *testing.T) {
	tstest.PanicOnLog()
	tstest.ResourceCheck(t)

	buf := bytes.Buffer{}
	err := WriteMsg(&buf, []byte("Test string1"))
	if err != nil {
		t.Fatalf("write1: %v\n", err)
	}
	err = WriteMsg(&buf, []byte(""))
	if err != nil {
		t.Fatalf("write2: %v\n", err)
	}
	err = WriteMsg(&buf, []byte("Test3"))
	if err != nil {
		t.Fatalf("write3: %v\n", err)
	}

	b, err := ReadMsg(&buf)
	if err != nil {
		t.Fatalf("read1 error: %v", err)
	}
	if want, got := "Test string1", string(b); want != got {
		t.Fatalf("read1: %#v != %#v\n", want, got)
	}
	b, err = ReadMsg(&buf)
	if err != nil {
		t.Fatalf("read2 error: %v", err)
	}
	if want, got := "", string(b); want != got {
		t.Fatalf("read2: %#v != %#v\n", want, got)
	}
	b, err = ReadMsg(&buf)
	if err != nil {
		t.Fatalf("read3 error: %v", err)
	}
	if want, got := "Test3", string(b); want != got {
		t.Fatalf("read3: %#v != %#v\n", want, got)
	}

	b, err = ReadMsg(&buf)
	if err == nil {
		t.Fatalf("read4: expected error, got %#v\n", b)
	}
}

func TestClientServer(t *testing.T) {
	tstest.PanicOnLog()
	tstest.ResourceCheck(t)

	b := &FakeBackend{}
	var bs *BackendServer
	var bc *BackendClient
	serverToClientCh := make(chan []byte, 16)
	defer close(serverToClientCh)
	go func() {
		for b := range serverToClientCh {
			bc.GotNotifyMsg(b)
		}
	}()
	serverToClient := func(n Notify) {
		b, err := json.Marshal(n)
		if err != nil {
			panic(err.Error())
		}
		serverToClientCh <- append([]byte{}, b...)
	}
	clientToServer := func(b []byte) {
		bs.GotCommandMsg(context.TODO(), b)
	}
	slogf := func(fmt string, args ...any) {
		t.Logf("s: "+fmt, args...)
	}
	clogf := func(fmt string, args ...any) {
		t.Logf("c: "+fmt, args...)
	}
	bs = NewBackendServer(slogf, b, serverToClient)
	// Verify that this doesn't break bs's callback:
	NewBackendServer(slogf, b, nil)
	bc = NewBackendClient(clogf, clientToServer)

	ch := make(chan Notify, 256)
	notify := func(n Notify) { ch <- n }
	h, err := NewHandle(bc, clogf, notify, Options{
		Prefs: &Prefs{
			ControlURL: "http://example.com/fake",
		},
	})
	if err != nil {
		t.Fatalf("NewHandle error: %v\n", err)
	}

	notes := Notify{}
	nn := []Notify{}
	processNote := func(n Notify) {
		nn = append(nn, n)
		if n.State != nil {
			t.Logf("state change: %v", *n.State)
			notes.State = n.State
		}
		if n.Prefs != nil {
			notes.Prefs = n.Prefs
		}
		if n.NetMap != nil {
			notes.NetMap = n.NetMap
		}
		if n.Engine != nil {
			notes.Engine = n.Engine
		}
		if n.BrowseToURL != nil {
			notes.BrowseToURL = n.BrowseToURL
		}
	}
	notesState := func() State {
		if notes.State != nil {
			return *notes.State
		}
		return NoState
	}

	flushUntil := func(wantFlush State) {
		t.Helper()
		timer := time.NewTimer(1 * time.Second)
	loop:
		for {
			select {
			case n := <-ch:
				processNote(n)
				if notesState() == wantFlush {
					break loop
				}
			case <-timer.C:
				t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
			}
		}
		timer.Stop()
	loop2:
		for {
			select {
			case n := <-ch:
				processNote(n)
			default:
				break loop2
			}
		}
		if got, want := h.State(), notesState(); got != want {
			t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
		}
	}

	flushUntil(NeedsLogin)

	h.StartLoginInteractive()
	flushUntil(Running)
	if notes.NetMap == nil && h.NetMap() != nil {
		t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
	}

	h.UpdatePrefs(func(p *Prefs) {
		p.WantRunning = false
	})
	flushUntil(Stopped)

	h.Logout()
	flushUntil(NeedsLogin)

	h.Login(&tailcfg.Oauth2Token{
		AccessToken: "google_id_token",
		TokenType:   GoogleIDTokenType,
	})
	flushUntil(Running)
}

func TestNilBackend(t *testing.T) {
	var called *Notify
	bs := NewBackendServer(t.Logf, nil, func(n Notify) {
		called = &n
	})
	bs.SendErrorMessage("Danger, Will Robinson!")
	if called == nil {
		t.Errorf("expect callback to be called, wasn't")
	}
	if called.ErrMessage == nil || *called.ErrMessage != "Danger, Will Robinson!" {
		t.Errorf("callback got wrong error: %v", called.ErrMessage)
	}
}
