// Copyright ©2017 The Gonum 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 graphql

import (
	"bytes"
	"errors"
	"fmt"
	"os/exec"
	"slices"
	"strconv"
	"strings"
	"testing"

	"gonum.org/v1/gonum/graph"
	"gonum.org/v1/gonum/graph/encoding"
	"gonum.org/v1/gonum/graph/encoding/dot"
	"gonum.org/v1/gonum/graph/simple"
)

var decodeTests = []struct {
	name    string
	json    string
	roots   map[uint64]bool
	wantDOT string
	wantErr error
}{
	{
		name: "starwars",
		json: starwars,
		roots: map[uint64]bool{
			0xa3cff1a4c3ef3bb6: true,
			0xb39aa14d66aedad5: true,
		},
		wantDOT: `strict digraph {
  // Node definitions.
  "0x8a10d5a2611fd03f" [name="Richard Marquand"];
  "0xa3cff1a4c3ef3bb6" [
    name="Star Wars: Episode V - The Empire Strikes Back"
    release_date="1980-05-21T00:00:00Z"
    revenue=534000000
    running_time=124
  ];
  "0xb39aa14d66aedad5" [
    name="Star Wars: Episode VI - Return of the Jedi"
    release_date="1983-05-25T00:00:00Z"
    revenue=572000000
    running_time=131
  ];
  "0x0312de17a7ee89f9" [name="Luke Skywalker"];
  "0x3da8d1dcab1bb381" [name="Han Solo"];
  "0x4a7d0b5fe91e78a4" [name="Irvin Kernshner"];
  "0x718337b9dcbaa7d9" [name="Princess Leia"];

  // Edge definitions.
  "0xa3cff1a4c3ef3bb6" -> "0x0312de17a7ee89f9" [label=starring];
  "0xa3cff1a4c3ef3bb6" -> "0x3da8d1dcab1bb381" [label=starring];
  "0xa3cff1a4c3ef3bb6" -> "0x4a7d0b5fe91e78a4" [label=director];
  "0xa3cff1a4c3ef3bb6" -> "0x718337b9dcbaa7d9" [label=starring];
  "0xb39aa14d66aedad5" -> "0x8a10d5a2611fd03f" [label=director];
  "0xb39aa14d66aedad5" -> "0x0312de17a7ee89f9" [label=starring];
  "0xb39aa14d66aedad5" -> "0x3da8d1dcab1bb381" [label=starring];
  "0xb39aa14d66aedad5" -> "0x718337b9dcbaa7d9" [label=starring];
}`,
	},
	{
		name: "tutorial",
		json: dgraphTutorial,
		roots: map[uint64]bool{
			0xfd90205a458151f:  true,
			0x52a80955d40ec819: true,
		},
		wantDOT: `strict digraph {
  // Node definitions.
  "0x892a6da7ee1fbdec" [
    age=55
    name=Sarah
  ];
  "0x99b74c1b5ab100ec" [
    age=35
    name=Artyom
  ];
  "0xb9e12a67e34d6acc" [
    age=19
    name=Catalina
  ];
  "0xbf104824c777525d" [name=Perro];
  "0xf590a923ea1fccaa" [name=Goldie];
  "0xf92d7dbe272d680b" [name="Hyung Sin"];
  "0x0fd90205a458151f" [
    age=39
    name=Michael
  ];
  "0x37734fcf0a6fcc69" [name="Rammy the sheep"];
  "0x52a80955d40ec819" [
    age=35
    name=Amit
  ];
  "0x5e9ad1cd9466228c" [
    age=24
    name="Sang Hyun"
  ];

  // Edge definitions.
  "0xb9e12a67e34d6acc" -> "0xbf104824c777525d" [label=owns_pet];
  "0xb9e12a67e34d6acc" -> "0x5e9ad1cd9466228c" [label=friend];
  "0xf92d7dbe272d680b" -> "0x5e9ad1cd9466228c" [label=friend];
  "0x0fd90205a458151f" -> "0x892a6da7ee1fbdec" [label=friend];
  "0x0fd90205a458151f" -> "0x99b74c1b5ab100ec" [label=friend];
  "0x0fd90205a458151f" -> "0xb9e12a67e34d6acc" [label=friend];
  "0x0fd90205a458151f" -> "0x37734fcf0a6fcc69" [label=owns_pet];
  "0x0fd90205a458151f" -> "0x52a80955d40ec819" [label=friend];
  "0x0fd90205a458151f" -> "0x5e9ad1cd9466228c" [label=friend];
  "0x52a80955d40ec819" -> "0x99b74c1b5ab100ec" [label=friend];
  "0x52a80955d40ec819" -> "0x0fd90205a458151f" [label=friend];
  "0x52a80955d40ec819" -> "0x5e9ad1cd9466228c" [label=friend];
  "0x5e9ad1cd9466228c" -> "0xb9e12a67e34d6acc" [label=friend];
  "0x5e9ad1cd9466228c" -> "0xf590a923ea1fccaa" [label=owns_pet];
  "0x5e9ad1cd9466228c" -> "0xf92d7dbe272d680b" [label=friend];
  "0x5e9ad1cd9466228c" -> "0x52a80955d40ec819" [label=friend];
}`,
	},
	{
		name:    "tutorial missing IDs",
		json:    dgraphTutorialMissingIDs,
		wantErr: errors.New("graphql: no UID for node"), // Incomplete error string.
	},
}

func TestDecode(t *testing.T) {
	for _, test := range decodeTests {
		dst := newDirectedGraph()
		err := Unmarshal([]byte(test.json), "_uid_", dst)
		if test.wantErr == nil && err != nil {
			t.Errorf("failed to unmarshal GraphQL JSON graph for %q: %v", test.name, err)
		} else if test.wantErr != nil {
			if err == nil {
				t.Errorf("expected error for %q: got:%v want:%v", test.name, err, test.wantErr)
			}
			continue
		}
		b, err := dot.Marshal(dst, "", "", "  ")
		if err != nil {
			t.Fatalf("failed to DOT marshal graph %q: %v", test.name, err)
		}
		gotDOT := string(b)
		if gotDOT != test.wantDOT {
			t.Errorf("unexpected DOT encoding for %q:\ngot:\n%s\nwant:\n%s", test.name, gotDOT, test.wantDOT)
		}
		checkDOT(t, b)
	}
}

type directedGraph struct {
	*simple.DirectedGraph
}

func newDirectedGraph() *directedGraph {
	return &directedGraph{DirectedGraph: simple.NewDirectedGraph()}
}

func (g *directedGraph) NewNode() graph.Node {
	return &node{attributes: make(attributes)}
}

func (g *directedGraph) NewEdge(from, to graph.Node) graph.Edge {
	return &edge{Edge: g.DirectedGraph.NewEdge(from, to)}
}

type node struct {
	id uint64
	attributes
}

func (n *node) ID() int64     { return int64(n.id) }
func (n *node) DOTID() string { return fmt.Sprintf("0x%016x", n.id) }

func (n *node) SetIDFromString(uid string) error {
	if !strings.HasPrefix(uid, "0x") {
		return fmt.Errorf("uid is not hex value: %q", uid)
	}
	var err error
	n.id, err = strconv.ParseUint(uid[2:], 16, 64)
	return err
}

type edge struct {
	graph.Edge
	label string
}

func (e *edge) SetLabel(l string) {
	e.label = l
}

func (e *edge) Attributes() []encoding.Attribute {
	return []encoding.Attribute{{Key: "label", Value: e.label}}
}

type attributes map[string]encoding.Attribute

func (a attributes) SetAttribute(attr encoding.Attribute) error {
	a[attr.Key] = attr
	return nil
}

func (a attributes) Attributes() []encoding.Attribute {
	keys := make([]string, 0, len(a))
	for k := range a {
		keys = append(keys, k)
	}
	slices.Sort(keys)
	attr := make([]encoding.Attribute, 0, len(keys))
	for _, k := range keys {
		v := a[k]
		if strings.Contains(v.Value, " ") {
			v.Value = `"` + v.Value + `"`
		}
		attr = append(attr, v)
	}
	return attr
}

// checkDOT hands b to the dot executable if it exists and fails t if dot
// returns an error.
func checkDOT(t *testing.T, b []byte) {
	dot, err := exec.LookPath("dot")
	if err != nil {
		t.Logf("skipping DOT syntax check: %v", err)
		return
	}
	cmd := exec.Command(dot)
	cmd.Stdin = bytes.NewReader(b)
	stderr := &bytes.Buffer{}
	cmd.Stderr = stderr
	err = cmd.Run()
	if err != nil {
		t.Errorf("invalid DOT syntax: %v\n%s\ninput:\n%s", err, stderr.String(), b)
	}
}
