// Copyright 2020 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vocabulary

import (
	"io/ioutil"
	"os"
	"os/exec"
	"testing"

	"github.com/golang/protobuf/jsonpb"

	discovery "github.com/google/gnostic/discovery"
	metrics "github.com/google/gnostic/metrics"
	openapiv2 "github.com/google/gnostic/openapiv2"
	openapiv3 "github.com/google/gnostic/openapiv3"
)

func fillTestProtoStructure(words []string, count []int) []*metrics.WordCount {
	counts := make([]*metrics.WordCount, 0)
	for i := 0; i < len(words); i++ {
		temp := &metrics.WordCount{
			Word:  words[i],
			Count: int32(count[i]),
		}
		counts = append(counts, temp)
	}
	return counts
}

func testVocabulary(t *testing.T, outputVocab *metrics.Vocabulary, referencePb *metrics.Vocabulary) {
	results := Difference([]*metrics.Vocabulary{outputVocab, referencePb})
	results2 := Difference([]*metrics.Vocabulary{referencePb, outputVocab})

	if !isEmpty(results) && !isEmpty(results2) {
		t.Logf("Difference failed: Output does not match")
		t.FailNow()
	} else {
		// if the test succeeded, clean up
		os.Remove("vocabulary-operation.pb")
	}
}

func testVocabularyOutput(t *testing.T, outputFile string, referenceFile string) {
	err := exec.Command("diff", outputFile, referenceFile).Run()
	if err != nil {
		t.Logf("Diff failed: %s vs %s %+v", outputFile, referenceFile, err)
		t.FailNow()
	} else {
		// if the test succeeded, clean up
		os.Remove(outputFile)
	}
}

func TestSampleVocabularyUnion(t *testing.T) {
	v1 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"heelo", "random", "funcName", "google"}, []int{1, 2, 3, 4}),
		Properties: fillTestProtoStructure([]string{"Hello", "dog", "funcName", "cat"}, []int{4, 3, 2, 1}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "print", "funcName"}, []int{12, 11, 4}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	v2 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "random", "status", "google"}, []int{5, 6, 1, 4}),
		Properties: fillTestProtoStructure([]string{"cat", "dog", "thing"}, []int{4, 3, 2}),
		Operations: fillTestProtoStructure([]string{"countPrint", "print", "funcName"}, []int{17, 12, 19}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	vocabularies := make([]*metrics.Vocabulary, 0)
	vocabularies = append(vocabularies, &v1, &v2)

	reference := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "funcName", "google", "heelo", "random", "status"}, []int{5, 3, 8, 1, 8, 1}),
		Properties: fillTestProtoStructure([]string{"Hello", "cat", "dog", "funcName", "thing"}, []int{4, 5, 6, 2, 2}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "countPrint", "funcName", "print"}, []int{12, 17, 23, 23}),
		Parameters: fillTestProtoStructure([]string{"id", "name", "suggester", "tag"}, []int{2, 10, 30, 2}),
	}

	unionResult := Union(vocabularies)

	testVocabulary(t,
		unionResult,
		&reference,
	)
}

func TestSampleVocabularyIntersection(t *testing.T) {
	v1 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"heelo", "random", "funcName", "google"}, []int{1, 2, 3, 4}),
		Properties: fillTestProtoStructure([]string{"Hello", "dog", "funcName", "cat"}, []int{4, 3, 2, 1}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "print", "funcName"}, []int{12, 11, 4}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	v2 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "random", "status", "google"}, []int{5, 6, 1, 4}),
		Properties: fillTestProtoStructure([]string{"cat", "dog", "thing"}, []int{4, 3, 2}),
		Operations: fillTestProtoStructure([]string{"countPrint", "print", "funcName"}, []int{17, 12, 19}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	vocabularies := make([]*metrics.Vocabulary, 0)
	vocabularies = append(vocabularies, &v1, &v2)

	reference := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"google", "random"}, []int{8, 8}),
		Properties: fillTestProtoStructure([]string{"cat", "dog"}, []int{5, 6}),
		Operations: fillTestProtoStructure([]string{"funcName", "print"}, []int{23, 23}),
		Parameters: fillTestProtoStructure([]string{"id", "name", "suggester", "tag"}, []int{2, 10, 30, 2}),
	}

	intersectionResult := Intersection(vocabularies)

	testVocabulary(t,
		intersectionResult,
		&reference,
	)
}
func TestSampleVocabularyDifference(t *testing.T) {
	v1 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"heelo", "random", "funcName", "google"}, []int{1, 2, 3, 4}),
		Properties: fillTestProtoStructure([]string{"Hello", "dog", "funcName", "cat"}, []int{4, 3, 2, 1}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "print", "funcName"}, []int{12, 11, 4}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	v2 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "random", "status", "google"}, []int{5, 6, 1, 4}),
		Properties: fillTestProtoStructure([]string{"cat", "dog", "thing"}, []int{4, 3, 2}),
		Operations: fillTestProtoStructure([]string{"countPrint", "print", "funcName"}, []int{17, 12, 19}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	vocabularies := make([]*metrics.Vocabulary, 0)
	vocabularies = append(vocabularies, &v1, &v2)

	reference := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"funcName", "heelo"}, []int{3, 1}),
		Properties: fillTestProtoStructure([]string{"Hello", "funcName"}, []int{4, 2}),
		Operations: fillTestProtoStructure([]string{"countGreetings"}, []int{12}),
	}

	differenceResult := Difference(vocabularies)

	testVocabulary(t,
		differenceResult,
		&reference,
	)
}

func TestSampleVocabularyFilterCommon(t *testing.T) {
	v1 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"heelo", "random", "funcName", "google"}, []int{1, 2, 3, 4}),
		Properties: fillTestProtoStructure([]string{"Hello", "dog", "funcName", "cat"}, []int{4, 3, 2, 1}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "print", "funcName"}, []int{12, 11, 4}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	v2 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "random", "status", "google"}, []int{5, 6, 1, 4}),
		Properties: fillTestProtoStructure([]string{"cat", "dog", "thing"}, []int{4, 3, 2}),
		Operations: fillTestProtoStructure([]string{"countPrint", "print", "funcName"}, []int{17, 12, 19}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	vocabularies := make([]*metrics.Vocabulary, 0)
	vocabularies = append(vocabularies, &v1, &v2)

	reference := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"funcName", "heelo"}, []int{3, 1}),
		Properties: fillTestProtoStructure([]string{"Hello", "funcName"}, []int{4, 2}),
		Operations: fillTestProtoStructure([]string{"countGreetings"}, []int{12}),
	}

	reference2 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"Hello", "status"}, []int{5, 1}),
		Properties: fillTestProtoStructure([]string{"thing"}, []int{2}),
		Operations: fillTestProtoStructure([]string{"countPrint"}, []int{17}),
	}

	differenceResult := FilterCommon(vocabularies)

	testVocabulary(t,
		differenceResult.Vocabularies[0],
		&reference,
	)

	testVocabulary(t,
		differenceResult.Vocabularies[1],
		&reference2,
	)
}

func TestSampleVocabularyCSV(t *testing.T) {
	v1 := metrics.Vocabulary{
		Schemas:    fillTestProtoStructure([]string{"heelo", "random", "funcName", "google"}, []int{1, 2, 3, 4}),
		Properties: fillTestProtoStructure([]string{"Hello", "dog", "funcName", "cat"}, []int{4, 3, 2, 1}),
		Operations: fillTestProtoStructure([]string{"countGreetings", "print", "funcName"}, []int{12, 11, 4}),
		Parameters: fillTestProtoStructure([]string{"name", "id", "tag", "suggester"}, []int{5, 1, 1, 15}),
	}

	WriteCSV(&v1, "")

	testVocabularyOutput(t,
		"vocabulary-operation.csv",
		"../../testdata/v3.0/csv/sample-vocabulary.csv",
	)
}

func TestSampleVocabularyV2(t *testing.T) {
	inputFile := "../../examples/v2.0/json/petstore.json"
	referenceFile := "../../testdata/metrics/vocabulary/petstore-v2.json"
	data, err := ioutil.ReadFile(inputFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	document, err := openapiv2.ParseDocument(data)
	if err != nil {
		t.Logf("Parse failed: %+v", err)
		t.FailNow()
	}
	v1 := NewVocabularyFromOpenAPIv2(document)
	// uncomment the following line to write reference data
	//err = ioutil.WriteFile(referenceFile, []byte(protojson.Format(v1)), 0644)
	referenceData, err := ioutil.ReadFile(referenceFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	reference := metrics.Vocabulary{}
	jsonpb.UnmarshalString(string(referenceData), &reference)
	testVocabulary(t,
		v1,
		&reference,
	)
}

func TestSampleVocabularyV3(t *testing.T) {
	inputFile := "../../examples/v3.0/json/petstore.json"
	referenceFile := "../../testdata/metrics/vocabulary/petstore-v3.json"
	data, err := ioutil.ReadFile(inputFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	document, err := openapiv3.ParseDocument(data)
	if err != nil {
		t.Logf("Parse failed: %+v", err)
		t.FailNow()
	}
	v1 := NewVocabularyFromOpenAPIv3(document)
	// uncomment the following line to write reference data
	//err = ioutil.WriteFile(referenceFile, []byte(protojson.Format(v1)), 0644)
	referenceData, err := ioutil.ReadFile(referenceFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	reference := metrics.Vocabulary{}
	jsonpb.UnmarshalString(string(referenceData), &reference)
	testVocabulary(t,
		v1,
		&reference,
	)
}

func TestSampleVocabularyDiscovery(t *testing.T) {
	inputFile := "../../examples/discovery/discovery-v1.json"
	referenceFile := "../../testdata/metrics/vocabulary/discovery.json"
	data, err := ioutil.ReadFile(inputFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	document, err := discovery.ParseDocument(data)
	if err != nil {
		t.Logf("Parse failed: %+v", err)
		t.FailNow()
	}
	v1 := NewVocabularyFromDiscovery(document)
	// uncomment the following line to write reference data
	//err = ioutil.WriteFile(referenceFile, []byte(protojson.Format(v1)), 0644)
	referenceData, err := ioutil.ReadFile(referenceFile)
	if err != nil {
		t.Logf("ReadFile failed: %+v", err)
		t.FailNow()
	}
	reference := metrics.Vocabulary{}
	jsonpb.UnmarshalString(string(referenceData), &reference)
	testVocabulary(t,
		v1,
		&reference,
	)
}
