package goparser_test

import (
	"context"
	"errors"
	"testing"

	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser/analyzererrors"
	goparser "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser/go"
)

const validGoSource = `package payments

import "fmt"

type Service struct{}

func (s *Service) Charge() {
	fmt.Println("charge")
	helper()
}

func helper() {}
`

const syntaxErrorSource = `package payments

func broken( {
}
`

func TestAnalyzerParsesValidGoFixture(t *testing.T) {
	t.Parallel()

	analyzer := goparser.NewAnalyzer()
	artifact, chunks, err := analyzer.ParseFile(context.Background(), []byte(validGoSource), "processor.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if artifact.SchemaVersion != graph.SchemaVersionV1 {
		t.Fatalf("schema_version: got %q, want v1", artifact.SchemaVersion)
	}

	if !hasNodeKind(artifact.Nodes, graph.NodeKindFile) {
		t.Fatal("expected file node")
	}
	if !hasNodeKind(artifact.Nodes, graph.NodeKindPackage) {
		t.Fatal("expected package node")
	}
	if !hasNodeWithName(artifact.Nodes, graph.NodeKindSymbol, "Service") {
		t.Fatal("expected Service type node")
	}
	if !hasNodeWithName(artifact.Nodes, graph.NodeKindSymbol, "Charge") {
		t.Fatal("expected Charge method node")
	}
	if !hasNodeWithName(artifact.Nodes, graph.NodeKindSymbol, "helper") {
		t.Fatal("expected helper function node")
	}

	if !hasEdgeKind(artifact.Edges, graph.EdgeKindContains) {
		t.Fatal("expected contains edge")
	}
	if !hasEdgeKind(artifact.Edges, graph.EdgeKindDefines) {
		t.Fatal("expected defines edge")
	}
	if !hasImportEdge(artifact.Edges, "pkg:fmt") {
		t.Fatal("expected imports edge to fmt")
	}
	if !hasCallEdge(artifact.Edges, "symbol:processor.go#Charge", "symbol:fmt#Println") {
		t.Fatal("expected Charge -> fmt.Println call edge")
	}
	if !hasCallEdge(artifact.Edges, "symbol:processor.go#Charge", "symbol:processor.go#helper") {
		t.Fatal("expected Charge -> helper call edge")
	}

	if len(chunks) != 3 {
		t.Fatalf("expected 3 chunks, got %d", len(chunks))
	}

	chunkByName := map[string]contractevents.CodeChunk{}
	for _, chunk := range chunks {
		chunkByName[chunk.SymbolName] = chunk
	}

	serviceChunk, ok := chunkByName["Service"]
	if !ok {
		t.Fatal("expected Service chunk")
	}
	if serviceChunk.SymbolType != contractevents.SymbolTypeType {
		t.Fatalf("Service symbol_type: got %q, want type", serviceChunk.SymbolType)
	}
	if serviceChunk.StartLine <= 0 || serviceChunk.EndLine < serviceChunk.StartLine {
		t.Fatalf("Service line range invalid: %d-%d", serviceChunk.StartLine, serviceChunk.EndLine)
	}

	chargeChunk := chunkByName["Charge"]
	if chargeChunk.SymbolType != contractevents.SymbolTypeMethod {
		t.Fatalf("Charge symbol_type: got %q, want method", chargeChunk.SymbolType)
	}
	if chargeChunk.StartLine <= 0 || chargeChunk.EndLine < chargeChunk.StartLine {
		t.Fatalf("Charge line range invalid: %d-%d", chargeChunk.StartLine, chargeChunk.EndLine)
	}
}

func TestAnalyzerSyntaxErrorReturnsDiagnostics(t *testing.T) {
	t.Parallel()

	analyzer := goparser.NewAnalyzer()
	_, _, err := analyzer.ParseFile(context.Background(), []byte(syntaxErrorSource), "broken.go")
	if err == nil {
		t.Fatal("expected parse error")
	}

	var parseErr *analyzererrors.ParseError
	if !errors.As(err, &parseErr) {
		t.Fatalf("expected ParseError, got %T", err)
	}
	if len(parseErr.Diagnostics) != 1 {
		t.Fatalf("expected 1 diagnostic, got %d", len(parseErr.Diagnostics))
	}

	diag := parseErr.Diagnostics[0]
	if diag.Code != "syntax_error" {
		t.Fatalf("code: got %q, want syntax_error", diag.Code)
	}
	if diag.Severity != contractevents.DiagnosticSeverityError {
		t.Fatalf("severity: got %q, want error", diag.Severity)
	}
	if diag.FilePath != "broken.go" {
		t.Fatalf("file_path: got %q, want broken.go", diag.FilePath)
	}
	if diag.Line <= 0 {
		t.Fatal("expected positive line number in diagnostic")
	}
}

func TestAnalyzerEmptyFileReturnsDiagnostics(t *testing.T) {
	t.Parallel()

	analyzer := goparser.NewAnalyzer()
	_, _, err := analyzer.ParseFile(context.Background(), []byte(""), "empty.go")
	if err == nil {
		t.Fatal("expected parse error for empty file")
	}

	diagnostics := analyzererrors.DiagnosticsFromError(err)
	if len(diagnostics) == 0 {
		t.Fatal("expected diagnostics-friendly error")
	}
	if diagnostics[0].Severity != contractevents.DiagnosticSeverityError {
		t.Fatalf("severity: got %q, want error", diagnostics[0].Severity)
	}
}

func TestAnalyzerRemainsStateless(t *testing.T) {
	t.Parallel()

	analyzer := goparser.NewAnalyzer()
	ctx := context.Background()

	first, _, err := analyzer.ParseFile(ctx, []byte(validGoSource), "a.go")
	if err != nil {
		t.Fatalf("first parse: %v", err)
	}
	second, _, err := analyzer.ParseFile(ctx, []byte(`package main

func main() {}
`), "b.go")
	if err != nil {
		t.Fatalf("second parse: %v", err)
	}

	if len(first.Nodes) == len(second.Nodes) && first.Nodes[0].Path == second.Nodes[0].Path {
		t.Fatal("expected independent parse results across files")
	}
}

func hasNodeKind(nodes []graph.Node, kind graph.NodeKind) bool {
	for _, node := range nodes {
		if node.Kind == kind {
			return true
		}
	}
	return false
}

func hasNodeWithName(nodes []graph.Node, kind graph.NodeKind, name string) bool {
	for _, node := range nodes {
		if node.Kind == kind && node.Name == name {
			return true
		}
	}
	return false
}

func hasEdgeKind(edges []graph.Edge, kind graph.EdgeKind) bool {
	for _, edge := range edges {
		if edge.Kind == kind {
			return true
		}
	}
	return false
}

func hasImportEdge(edges []graph.Edge, targetID string) bool {
	for _, edge := range edges {
		if edge.Kind == graph.EdgeKindImports && edge.TargetID == targetID {
			return true
		}
	}
	return false
}

func hasCallEdge(edges []graph.Edge, sourceID, targetID string) bool {
	for _, edge := range edges {
		if edge.Kind == graph.EdgeKindCalls && edge.SourceID == sourceID && edge.TargetID == targetID {
			return true
		}
	}
	return false
}
