package webhookdemo

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gorilla/websocket"
	"github.com/graphql-go/graphql"
)

func TestWebSocket(t *testing.T) {
	var workers sync.WaitGroup
	pong := make(chan struct{}, 4)
	up := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return r.Header.Get("Origin") == "https://app.example" }}
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := up.Upgrade(w, r, nil)
		if err != nil {
			return
		}
		workers.Add(1)
		defer workers.Done()
		defer c.Close()
		c.SetReadLimit(64)
		_ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
		c.SetPongHandler(func(string) error { pong <- struct{}{}; return nil })
		if err := c.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(time.Second)); err != nil {
			return
		}
		for {
			kind, b, err := c.ReadMessage()
			if err != nil {
				return
			}
			_ = c.SetWriteDeadline(time.Now().Add(time.Second))
			if err := c.WriteMessage(kind, b); err != nil {
				return
			}
		}
	}))
	defer s.Close()
	url := "ws" + strings.TrimPrefix(s.URL, "http")
	dialer := websocket.Dialer{HandshakeTimeout: time.Second}
	_, resp, err := dialer.Dial(url, http.Header{"Origin": []string{"https://evil.example"}})
	if err == nil || resp.StatusCode != 403 {
		t.Fatalf("%v %v", resp, err)
	}
	resp.Body.Close()
	t.Log("rejected origin: 403")
	c, _, err := dialer.Dial(url, http.Header{"Origin": []string{"https://app.example"}})
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	_ = c.SetReadDeadline(time.Now().Add(2 * time.Second))
	if err := c.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil {
		t.Fatal(err)
	}
	_, body, err := c.ReadMessage()
	if err != nil || string(body) != "hello" {
		t.Fatalf("%q %v", body, err)
	}
	select {
	case <-pong:
	case <-time.After(time.Second):
		t.Fatal("pong not read")
	}
	t.Log("echo: hello; ping/pong processed")
	if err := c.WriteMessage(websocket.TextMessage, []byte(strings.Repeat("x", 65))); err != nil {
		t.Fatal(err)
	}
	_, _, err = c.ReadMessage()
	if !websocket.IsCloseError(err, websocket.CloseMessageTooBig) {
		t.Fatal(err)
	}
	t.Log("oversized message: close 1009")
	c.Close()
	workers.Wait()
}

type cacheKey struct{}
type requestCache struct {
	mu     sync.Mutex
	values map[string]any
}

func TestGraphQLSelectionAndErrors(t *testing.T) {
	var loads int
	user := graphql.NewObject(graphql.ObjectConfig{Name: "User", Fields: graphql.Fields{
		"name":    &graphql.Field{Type: graphql.String},
		"private": &graphql.Field{Type: graphql.String, Resolve: func(graphql.ResolveParams) (any, error) { return nil, errors.New("forbidden") }},
	}})
	post := graphql.NewObject(graphql.ObjectConfig{Name: "Post", Fields: graphql.Fields{
		"id": &graphql.Field{Type: graphql.ID},
		"author": &graphql.Field{Type: user, Resolve: func(p graphql.ResolveParams) (any, error) {
			key := p.Source.(map[string]any)["author_id"].(string)
			cache, _ := p.Context.Value(cacheKey{}).(*requestCache)
			if cache != nil {
				cache.mu.Lock()
				defer cache.mu.Unlock()
				if v, ok := cache.values[key]; ok {
					return v, nil
				}
			}
			loads++
			v := map[string]any{"name": "Gopher"}
			if cache != nil {
				cache.values[key] = v
			}
			return v, nil
		}},
	}})
	schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: graphql.NewObject(graphql.ObjectConfig{Name: "Query", Fields: graphql.Fields{
		"posts": &graphql.Field{Type: graphql.NewList(post), Resolve: func(graphql.ResolveParams) (any, error) {
			return []map[string]any{{"id": "1", "author_id": "7"}, {"id": "2", "author_id": "7"}}, nil
		}},
	}})})
	if err != nil {
		t.Fatal(err)
	}
	query := `{ posts { id author { name } } }`
	for _, cached := range []bool{false, true} {
		loads = 0
		ctx := context.Background()
		if cached {
			ctx = context.WithValue(ctx, cacheKey{}, &requestCache{values: map[string]any{}})
		}
		result := graphql.Do(graphql.Params{Schema: schema, RequestString: query, Context: ctx})
		if len(result.Errors) != 0 {
			t.Fatal(result.Errors)
		}
		want := 2
		if cached {
			want = 1
		}
		if loads != want {
			t.Fatal(loads)
		}
		b, _ := json.Marshal(result.Data)
		t.Logf("cache=%v author loads=%d data=%s", cached, loads, b)
	}
	result := graphql.Do(graphql.Params{Schema: schema, RequestString: `{ posts { id author { name private } } }`, Context: context.Background()})
	if len(result.Errors) != 2 || result.Data == nil {
		t.Fatal(result)
	}
	t.Logf("field errors=%d; partial data present=%v", len(result.Errors), result.Data != nil)
	result = graphql.Do(graphql.Params{Schema: schema, RequestString: `{ posts { missing } }`, Context: context.Background()})
	if len(result.Errors) == 0 {
		t.Fatal("invalid query accepted")
	}
	t.Log("unknown field rejected before execution")
}
