package aql

import (
	"encoding/json"
	"errors"
	"strings"
	"testing"

	"git.science.uu.nl/datastrophe/query-conversion/entity"
	"github.com/stretchr/testify/assert"
)

func TestEmptyQueryConversion(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [],
			"relations": []
		},
		"entities": [],
		"relations": [],
		"limit": 5000
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `
LET nodes = first(RETURN UNION_DISTINCT([],[]))
LET edges = first(RETURN UNION_DISTINCT([],[]))
RETURN {"vertices":nodes, "edges":edges }`
	assert.Equal(t, correctConvertedResult, *convertedResult)
}

func TestEntityOneAttributeQuery(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [],
		"limit": 5000
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestRelationWithConstraint(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"entityFrom": 0,
				"entityTo": -1,
				"constraints": [
					{
						"attribute": "Day",
						"value": "15",
						"dataType": "number",
						"matchType": "EQ"
					}
				]
			}
		],
		"limit": 5000
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestModifierCountEntity(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [],
		"limit": 5000,
		"modifiers": [
			{
				"type": "COUNT",
				"selectedType": "entity",
				"id": 0,
				"attributeIndex": -1
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN LENGTH (n0)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
func TestModifierCountEntityAttribute(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [],
		"limit": 5000,
		"modifiers": [
			{
				"type": "SUM",
				"selectedType": "entity",
				"id": 0,
				"attributeIndex": 0
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN SUM (n0[*].state)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
func TestModifierCountRelation(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"entityFrom": 0,
				"entityTo": -1,
				"constraints": [
					{
						"attribute": "Day",
						"value": "15",
						"dataType": "number",
						"matchType": "EQ"
					}
				]
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "COUNT",
				"selectedType": "relation",
				"id": 0,
				"attributeIndex": -1
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN LENGTH (unique(r0[*].edges[**]))`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
func TestModifierCountRelationAttribute(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"entityFrom": 0,
				"entityTo": -1,
				"constraints": [
					{
						"attribute": "Day",
						"value": "15",
						"dataType": "number",
						"matchType": "EQ"
					}
				]
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "AVG",
				"selectedType": "relation",
				"id": 0,
				"attributeIndex": 0
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN AVG (r0[*].edges[**].Day)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestRelationWithInOutConstraint(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0,
				1
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "city",
						"value": "San Francisco",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			},
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 3
				},
				"entityFrom": 1,
				"entityTo": 0,
				"constraints": [
					{
						"attribute": "Day",
						"value": "15",
						"dataType": "number",
						"matchType": "EQ"
					}
				]
			}
		],
		"limit": 5000
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n1 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestTwoRelations(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0,
				1,
				2
			],
			"relations": [
				0,
				1
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "city",
						"value": "New York",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			},
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "city",
						"value": "San Francisco",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			},
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
						"dataType": "text",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 3
				},
				"entityFrom": 2,
				"entityTo": 1,
				"constraints": [
					{
						"attribute": "Day",
						"value": "15",
						"dataType": "number",
						"matchType": "EQ"
					}
				]
			},
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"entityFrom": 0,
				"entityTo": -1,
				"constraints": []
			}
		],
		"limit": 5000
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n2 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n2 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET n0 = (FOR x IN airports FILTER x.city == "New York" RETURN x)LET r1 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), flatten(r1[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), flatten(r1[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestRelationWithOnlyToNode(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0
				],
				"relations": [
					0
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"relations": [
				{
					"type": "flights",
					"depth": {
						"min": 1,
						"max": 1
					},
					"entityFrom": -1,
					"entityTo": 0,
					"constraints": []
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 INBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestTooManyReturnEntities(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0,
					1,
					2
				],
				"relations": [
					0
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"relations": [
				{
					"type": "flights",
					"depth": {
						"min": 1,
						"max": 1
					},
					"entityFrom": -1,
					"entityTo": 0,
					"constraints": []
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	_, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.Equal(t, errors.New("non-existing entity referenced in return"), err)
}

func TestTooManyReturnRelations(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0
				],
				"relations": [
					0,
					1,
					2
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"relations": [
				{
					"type": "flights",
					"depth": {
						"min": 1,
						"max": 1
					},
					"entityFrom": -1,
					"entityTo": 0,
					"constraints": []
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	_, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.Equal(t, errors.New("non-existing relation referenced in return"), err)
}

func TestNegativeReturnEntities(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0,
					-1
				],
				"relations": [
					0,
					1,
					2
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"relations": [
				{
					"type": "flights",
					"depth": {
						"min": 1,
						"max": 1
					},
					"entityFrom": -1,
					"entityTo": 0,
					"constraints": []
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	_, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.Equal(t, errors.New("non-existing entity referenced in return"), err)
}

func TestNoRelationsField(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
			"return": {
				"entities": [
					0
				],
				"relations": [
					0
				]
			},
			"entities": [
				{
					"type": "airports",
					"constraints": [
						{
							"attribute": "city",
							"value": "San Francisco",
							"dataType": "text",
							"matchType": "exact"
						}
					]
				}
			],
			"relations": [
				{
					"type": "flights",
					"depth": {
						"min": 1,
						"max": 1
					},
					"entityFrom": -4,
					"entityTo": 0,
					"constraints": []
				}
			],
			"limit": 5000
		}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	_, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)
}