package exporter

import (
	"bytes"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/zzet/gortex/internal/graph"
)

func TestCypher_NodeShape(t *testing.T) {
	g := buildSampleGraph(t)

	var buf bytes.Buffer
	_, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)

	out := buf.String()
	// Each node should appear as a labeled CREATE.
	assert.Contains(t, out, "CREATE (:Function:GortexNode")
	assert.Contains(t, out, "CREATE (:Method:GortexNode")
	assert.Contains(t, out, "CREATE (:Field:GortexNode")

	// Required props on every node.
	assert.Contains(t, out, "id: 'main.go::F'")
	assert.Contains(t, out, "name: 'F'")
	assert.Contains(t, out, "qual_name: 'example.com/m.F'")
	assert.Contains(t, out, "file_path: 'main.go'")
	assert.Contains(t, out, "start_line: 3")
	assert.Contains(t, out, "language: 'go'")
	assert.Contains(t, out, "repo_prefix: 'example'")

	// Meta keys flattened onto the node, JSON for nested values.
	assert.Contains(t, out, "visibility: 'public'")
	assert.Contains(t, out, "complexity: 4")
	assert.Contains(t, out, `tags: '["hot","core"]'`)

	// Pure-statement format: no comment lines, no DDL. The first line of
	// the output should be a CREATE statement, not a `//` header.
	assert.NotContains(t, out, "//", "output must not contain comments — Memgraph's .cypherl loader rejects them")
	assert.NotContains(t, out, "CREATE INDEX", "DDL syntax differs between Neo4j and Memgraph; keep the file portable")
	assert.True(t, strings.HasPrefix(out, "CREATE (:"),
		"first line should be a CREATE statement; got: %q", out[:min(60, len(out))])
}

func TestCypher_EdgeShape(t *testing.T) {
	g := buildSampleGraph(t)

	var buf bytes.Buffer
	_, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)

	out := buf.String()
	assert.Contains(t, out, "MATCH (a:GortexNode {id: 'main.go::F'}), (b:GortexNode {id: 'lib.go::Logger.Print'}) CREATE (a)-[:CALLS")
	assert.Contains(t, out, "confidence: 1")
	assert.Contains(t, out, "confidence_label: 'EXTRACTED'")
	assert.Contains(t, out, "origin: 'ast_resolved'")
	assert.Contains(t, out, "line: 4")

	// Writes edge with meta.
	assert.Contains(t, out, ":WRITES")
	assert.Contains(t, out, "receiver_type: 'Logger'")
	assert.Contains(t, out, "origin: 'ast_inferred'")
}

func TestCypher_StringEscaping(t *testing.T) {
	g := graph.New()
	g.AddNode(&graph.Node{
		ID: "x.go::Quote", Kind: graph.KindFunction, Name: "Quote",
		FilePath: "x.go", StartLine: 1, EndLine: 1, Language: "go",
		Meta: map[string]any{
			"doc": "It's a \"thing\"\n\twith escapes.",
		},
	})
	var buf bytes.Buffer
	_, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)

	out := buf.String()
	// Single-quote should be backslash-escaped.
	assert.Contains(t, out, `\'`)
	// Newline should be \n literal in the source, not an actual newline inside the string.
	assert.Contains(t, out, `\n`)
	assert.Contains(t, out, `\t`)
}

func TestCypher_StatsCount(t *testing.T) {
	g := buildSampleGraph(t)

	var buf bytes.Buffer
	stats, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)
	assert.Equal(t, 3, stats.NodesWritten)
	assert.Equal(t, 2, stats.EdgesWritten)
	assert.Equal(t, int64(buf.Len()), stats.BytesWritten)
}

func TestCypher_EmptyGraph(t *testing.T) {
	g := graph.New()
	var buf bytes.Buffer
	stats, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)
	assert.Equal(t, 0, stats.NodesWritten)
	assert.Equal(t, 0, stats.EdgesWritten)
	// Empty graph produces an empty file. .cypherl loaders accept this;
	// any header / DDL is the CLI's responsibility (printed to stderr).
	assert.Empty(t, buf.String())
}

func TestCypher_FilterRemovesDanglingEdges(t *testing.T) {
	g := buildSampleGraph(t)

	var buf bytes.Buffer
	_, err := WriteCypher(&buf, g, Options{Kinds: []graph.NodeKind{graph.KindFunction}})
	require.NoError(t, err)

	out := buf.String()
	assert.Contains(t, out, "CREATE (:Function:GortexNode")
	assert.NotContains(t, out, "CREATE (:Method:GortexNode")
	assert.NotContains(t, out, ":CALLS",
		"edges with excluded endpoints must be dropped, not produce dangling MATCH")
}

func TestCypher_PropertyNameSanitization(t *testing.T) {
	g := graph.New()
	g.AddNode(&graph.Node{
		ID: "x.go::F", Kind: graph.KindFunction, Name: "F",
		FilePath: "x.go", StartLine: 1, EndLine: 1, Language: "go",
		Meta: map[string]any{
			"with-dash":   1,
			"with space":  2,
			"normal":      3,
			"@@@invalid":  4, // will sanitize to "___invalid"
			"meta.dotted": 5,
		},
	})
	var buf bytes.Buffer
	_, err := WriteCypher(&buf, g, Options{})
	require.NoError(t, err)

	out := buf.String()
	assert.Contains(t, out, "with_dash: 1")
	assert.Contains(t, out, "with_space: 2")
	assert.Contains(t, out, "normal: 3")
	assert.Contains(t, out, "meta_dotted: 5")
	// Hyphen / space / dot should never appear inside property keys.
	for _, line := range strings.Split(out, "\n") {
		// We only care about the property-map portion of CREATE statements.
		if !strings.Contains(line, "CREATE (:") {
			continue
		}
		// A key always looks like ` <ident>:`. None of our sanitized keys
		// should contain a non-identifier character.
		assert.NotContains(t, line, " with-dash:")
		assert.NotContains(t, line, " with space:")
		assert.NotContains(t, line, " meta.dotted:")
	}
}
