// Copyright ©2018 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 cytoscapejs

import (
	"encoding/json"
	"os"
	"path/filepath"
	"reflect"
	"testing"
)

var cytoscapejsElementsTests = []struct {
	path           string
	wantNodes      int
	wantEdges      int
	wantGraph      []Element
	wantAttributes []string
}{
	{
		path:      "edge-type.json",
		wantNodes: 10,
		wantEdges: 10,
		wantGraph: []Element{
			{Data: ElemData{ID: "n01", Attributes: map[string]interface{}{"type": "bezier"}}},
			{Data: ElemData{ID: "n02"}},
			{Data: ElemData{ID: "e01", Source: "n01", Target: "n02"}, Classes: "bezier"},
			{Data: ElemData{ID: "e02", Source: "n01", Target: "n02"}, Classes: "bezier"},
			{Data: ElemData{ID: "e03", Source: "n02", Target: "n01"}, Classes: "bezier"},
			{Data: ElemData{ID: "n03", Attributes: map[string]interface{}{"type": "unbundled-bezier"}}},
			{Data: ElemData{ID: "n04"}},
			{Data: ElemData{ID: "e04", Source: "n03", Target: "n04"}, Classes: "unbundled-bezier"},
			{Data: ElemData{ID: "n05", Attributes: map[string]interface{}{"type": "unbundled-bezier(multiple)"}}},
			{Data: ElemData{ID: "n06"}},
			{Data: ElemData{ID: "e05", Source: "n05", Target: "n06", Parent: ""}, Classes: "multi-unbundled-bezier"},
			{Data: ElemData{ID: "n07", Attributes: map[string]interface{}{"type": "haystack"}}},
			{Data: ElemData{ID: "n08"}},
			{Data: ElemData{ID: "e06", Source: "n08", Target: "n07"}, Classes: "haystack"},
			{Data: ElemData{ID: "e07", Source: "n08", Target: "n07"}, Classes: "haystack"},
			{Data: ElemData{ID: "e08", Source: "n08", Target: "n07"}, Classes: "haystack"},
			{Data: ElemData{ID: "e09", Source: "n08", Target: "n07"}, Classes: "haystack"},
			{Data: ElemData{ID: "n09", Attributes: map[string]interface{}{"type": "segments"}}},
			{Data: ElemData{ID: "n10"}},
			{Data: ElemData{ID: "e10", Source: "n09", Target: "n10"}, Classes: "segments"},
		},
	},
}

func TestUnmarshalElements(t *testing.T) {
	for _, test := range cytoscapejsElementsTests {
		data, err := os.ReadFile(filepath.Join("testdata", test.path))
		if err != nil {
			t.Errorf("failed to read %q: %v", test.path, err)
			continue
		}
		var got []Element
		err = json.Unmarshal(data, &got)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		var gotNodes, gotEdges int
		for _, e := range got {
			typ, err := e.Type()
			if err != nil {
				t.Errorf("unexpected error finding element type for %+v: %v", e, err)
			}
			switch typ {
			case NodeElement:
				gotNodes++
			case EdgeElement:
				gotEdges++
			}
		}

		if gotNodes != test.wantNodes {
			t.Errorf("unexpected result for order of %q: got:%d want:%d", test.path, gotNodes, test.wantNodes)
		}
		if gotEdges != test.wantEdges {
			t.Errorf("unexpected result for size of %q: got:%d want:%d", test.path, gotEdges, test.wantEdges)
		}
		if test.wantGraph != nil && !reflect.DeepEqual(got, test.wantGraph) {
			t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, test.wantGraph)
		}
	}
}

func TestMarshalElements(t *testing.T) {
	for _, test := range cytoscapejsElementsTests {
		data, err := os.ReadFile(filepath.Join("testdata", test.path))
		if err != nil {
			t.Errorf("failed to read %q: %v", test.path, err)
			continue
		}
		var want []Element
		err = json.Unmarshal(data, &want)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		marshaled, err := json.Marshal(want)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		var got []Element
		err = json.Unmarshal(marshaled, &got)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		if !reflect.DeepEqual(got, want) {
			t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, want)
		}
	}
}

var cytoscapejsNodeEdgeTests = []struct {
	path               string
	wantNodes          int
	wantEdges          int
	wantGraph          *Elements
	firstNode          Node
	firstEdge          Edge
	wantNodeAttributes map[string]bool
	wantEdgeAttributes map[string]bool
}{
	{
		path:      "cola-compound.json",
		wantNodes: 9,
		wantEdges: 7,
		wantGraph: &Elements{
			Nodes: []Node{
				{Data: NodeData{ID: "compound-1", Parent: ""}},
				{Data: NodeData{ID: "compound-2", Parent: ""}},
				{Data: NodeData{ID: "compound-3", Parent: ""}},
				{Data: NodeData{ID: "b", Parent: "compound-1"}},
				{Data: NodeData{ID: "c", Parent: "compound-1"}},
				{Data: NodeData{ID: "a", Parent: "compound-2"}},
				{Data: NodeData{ID: "d", Parent: "compound-3"}},
				{Data: NodeData{ID: "e", Parent: "compound-3"}},
				{Data: NodeData{ID: "f", Parent: ""}},
			},
			Edges: []Edge{
				{Data: EdgeData{ID: "ab", Source: "a", Target: "b"}},
				{Data: EdgeData{ID: "bc", Source: "b", Target: "c"}},
				{Data: EdgeData{ID: "ac", Source: "a", Target: "c"}},
				{Data: EdgeData{ID: "cd", Source: "c", Target: "d"}},
				{Data: EdgeData{ID: "de", Source: "d", Target: "e"}},
				{Data: EdgeData{ID: "df", Source: "d", Target: "f"}},
				{Data: EdgeData{ID: "af", Source: "a", Target: "f"}},
			},
		},
	},
	{
		path:      "tokyo-railways.json",
		wantNodes: 943,
		wantEdges: 860,
		firstNode: Node{
			Data: NodeData{
				ID: "8220",
				Attributes: map[string]interface{}{
					"station_name": "京成高砂",
					"close_ymd":    "",
					"lon":          139.866875,
					"post":         "",
					"e_status":     0.0,
					"SUID":         8220.0,
					"station_g_cd": 2300110.0,
					"add":          "東京都葛飾区高砂五丁目28-1",
					"line_cd":      99340.0,
					"selected":     false,
					"open_ymd":     "",
					"name":         "9934001",
					"pref_name":    "東京都",
					"shared_name":  "9934001",
					"lat":          35.750932,
					"x":            1398668.75,
					"y":            -357509.32,
				},
			},
			Position: &Position{
				X: 1398668.75,
				Y: -357509.32,
			},
		},
		firstEdge: Edge{
			Data: EdgeData{
				ID:     "18417",
				Source: "8220",
				Target: "8221",
				Attributes: map[string]interface{}{
					"line_name_k":        "ホクソウテツドウホクソウセン",
					"is_bullet":          false,
					"lon":                140.03784499075186,
					"company_name_k":     "ホクソウテツドウ",
					"zoom":               11.0,
					"SUID":               18417.0,
					"company_type":       0.0,
					"company_name_h":     "北総鉄道株式会社",
					"interaction":        "99340",
					"shared_interaction": "99340",
					"company_url":        "http://www.hokuso-railway.co.jp/",
					"line_name":          "北総鉄道北総線",
					"selected":           false,
					"company_name":       "北総鉄道",
					"company_cd":         152.0,
					"name":               "9934001 (99340) 9934002",
					"rr_cd":              99.0,
					"company_name_r":     "北総鉄道",
					"e_status_x":         0.0,
					"shared_name":        "9934001 (99340) 9934002",
					"lat":                35.78346285846615,
					"e_status_y":         0.0,
					"line_name_h":        "北総鉄道北総線",
				},
			},
		},
		wantNodeAttributes: map[string]bool{
			"station_name": true,
			"close_ymd":    true,
			"lon":          true,
			"post":         true,
			"e_status":     true,
			"SUID":         true,
			"station_g_cd": true,
			"add":          true,
			"line_cd":      true,
			"selected":     true,
			"open_ymd":     true,
			"name":         true,
			"pref_name":    true,
			"shared_name":  true,
			"lat":          true,
			"x":            true,
			"y":            true,
		},
		wantEdgeAttributes: map[string]bool{
			"line_name_k":        true,
			"is_bullet":          true,
			"lon":                true,
			"company_name_k":     true,
			"zoom":               true,
			"SUID":               true,
			"company_type":       true,
			"company_name_h":     true,
			"interaction":        true,
			"shared_interaction": true,
			"company_url":        true,
			"line_name":          true,
			"selected":           true,
			"company_name":       true,
			"company_cd":         true,
			"name":               true,
			"rr_cd":              true,
			"company_name_r":     true,
			"e_status_x":         true,
			"shared_name":        true,
			"lat":                true,
			"e_status_y":         true,
			"line_name_h":        true,
		},
	},
}

func TestUnmarshalNodeEdge(t *testing.T) {
	for _, test := range cytoscapejsNodeEdgeTests {
		data, err := os.ReadFile(filepath.Join("testdata", test.path))
		if err != nil {
			t.Errorf("failed to read %q: %v", test.path, err)
			continue
		}
		var got Elements
		err = json.Unmarshal(data, &got)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}

		if len(got.Nodes) != test.wantNodes {
			t.Errorf("unexpected result for order of %q: got:%d want:%d", test.path, len(got.Nodes), test.wantNodes)
		}
		if len(got.Edges) != test.wantEdges {
			t.Errorf("unexpected result for size of %q: got:%d want:%d", test.path, len(got.Edges), test.wantEdges)
		}
		if test.wantGraph != nil {
			if !reflect.DeepEqual(&got, test.wantGraph) {
				t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got.Nodes, test.wantGraph.Nodes)
			}
		} else {
			if !reflect.DeepEqual(got.Nodes[0], test.firstNode) {
				t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got.Nodes[0], test.firstNode)
			}
			if !reflect.DeepEqual(got.Edges[0], test.firstEdge) {
				t.Errorf("unexpected result for %q:\ngot:\n%v\nwant:\n%#v", test.path, got.Edges[0].Data.Source, test.firstEdge.Data.Source)
			}
		}
		if test.wantNodeAttributes != nil {
			var paths []string
			for _, n := range got.Nodes {
				paths = attrPaths(paths, "", n.Data.Attributes)
			}
			gotAttrs := make(map[string]bool)
			for _, p := range paths {
				gotAttrs[p] = true
			}
			if !reflect.DeepEqual(gotAttrs, test.wantNodeAttributes) {
				t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, gotAttrs, test.wantNodeAttributes)
			}
		}
		if test.wantEdgeAttributes != nil {
			var paths []string
			for _, e := range got.Edges {
				paths = attrPaths(paths, "", e.Data.Attributes)
			}
			gotAttrs := make(map[string]bool)
			for _, p := range paths {
				gotAttrs[p] = true
			}
			if !reflect.DeepEqual(gotAttrs, test.wantEdgeAttributes) {
				t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, gotAttrs, test.wantEdgeAttributes)
			}
		}
	}
}

func TestMarshalNodeEdge(t *testing.T) {
	for _, test := range cytoscapejsNodeEdgeTests {
		data, err := os.ReadFile(filepath.Join("testdata", test.path))
		if err != nil {
			t.Errorf("failed to read %q: %v", test.path, err)
			continue
		}
		var want Elements
		err = json.Unmarshal(data, &want)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		marshaled, err := json.Marshal(want)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		var got Elements
		err = json.Unmarshal(marshaled, &got)
		if err != nil {
			t.Errorf("failed to unmarshal %q: %v", test.path, err)
			continue
		}
		if !reflect.DeepEqual(got, want) {
			t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, want)
		}
	}
}

func attrPaths(dst []string, prefix string, m map[string]interface{}) []string {
	for k, v := range m {
		path := prefix
		if path != "" {
			path += "."
		}
		if v, ok := v.(map[string]interface{}); ok {
			dst = attrPaths(dst, path+k, v)
		}
		dst = append(dst, path+k)
	}
	return dst
}
