package service

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

func loadJSONTemplate(t *testing.T, name string) map[string]any {
    t.Helper()
    path := filepath.Join("..", "..", "templates", "standard_templates", name+".json")
    body, err := os.ReadFile(path)
    if err != nil {
        t.Fatalf("read %s: %v", name, err)
    }
    var raw map[string]any
    if err := json.Unmarshal(body, &raw); err != nil {
        t.Fatalf("parse %s: %v", name, err)
    }
    return raw
}

func TestComposeTemplates_SingleTemplateIdentity(t *testing.T) {
    face := loadJSONTemplate(t, "std_face_recognition_stream")
    result, err := ComposeTemplates([]map[string]any{face})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result["name"] == nil {
        t.Error("result has no name")
    }
}

func TestComposeTemplates_FacePlusShoe(t *testing.T) {
    face := loadJSONTemplate(t, "std_face_recognition_stream")
    shoe := loadJSONTemplate(t, "std_workshoe_detection_stream")

    result, err := ComposeTemplates([]map[string]any{face, shoe})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    tmpl, _ := result["template"].(map[string]any)
    if tmpl == nil {
        t.Fatal("result missing template body")
    }

    nodes, _ := tmpl["nodes"].([]any)
    edges, _ := tmpl["edges"].([]any)

    t.Logf("composed name: %v", result["name"])
    t.Logf("nodes: %d, edges: %d", len(nodes), len(edges))

    // Should have more nodes than either input
    if len(nodes) < 10 {
        t.Errorf("expected at least 10 nodes, got %d", len(nodes))
    }
    if len(edges) < 10 {
        t.Errorf("expected at least 10 edges, got %d", len(edges))
    }

    // Verify no duplicate node IDs
    ids := map[string]bool{}
    for _, n := range nodes {
        node, _ := n.(map[string]any)
        id := stringValue(node["id"])
        if id == "" {
            t.Error("node with empty id")
            continue
        }
        if ids[id] {
            t.Errorf("duplicate node id: %s", id)
        }
        ids[id] = true
    }

    // Verify edges refer to existing nodes
    for _, e := range edges {
        edge, _ := e.([]any)
        if len(edge) < 2 {
            t.Error("edge too short")
            continue
        }
        from := stringValue(edge[0])
        to := stringValue(edge[1])
        if !ids[from] {
            t.Errorf("edge from unknown node: %s", from)
        }
        if !ids[to] {
            t.Errorf("edge to unknown node: %s", to)
        }
    }

    // Verify it can be serialized
    body, err := json.MarshalIndent(result, "", "  ")
    if err != nil {
        t.Errorf("marshal composed template: %v", err)
    }
    t.Logf("composed JSON length: %d bytes", len(body))

    // Verify key node types exist
    hasSource := false
    hasPreprocess := false
    hasPublish := false
    for _, n := range nodes {
        node, _ := n.(map[string]any)
        t := stringValue(node["type"])
        switch {
        case strings.HasPrefix(t, "input_"):
            hasSource = true
        case t == "preprocess":
            hasPreprocess = true
        case t == "publish":
            hasPublish = true
        }
    }
    if !hasSource {
        t.Error("composed template missing source node")
    }
    if !hasPreprocess {
        t.Error("composed template missing preprocess node")
    }
    if !hasPublish {
        t.Error("composed template missing publish node")
    }
}

func TestComposeTemplates_Empty(t *testing.T) {
    _, err := ComposeTemplates(nil)
    if err == nil {
        t.Fatal("expected error for nil input")
    }
}

func TestParseTemplateDAG_Invalid(t *testing.T) {
    _, err := parseTemplateDAG(map[string]any{})
    if err == nil {
        t.Fatal("expected error for empty raw")
    }

    _, err = parseTemplateDAG(map[string]any{
        "name": "test",
        "template": map[string]any{
            "nodes": []any{},
        },
    })
    if err == nil {
        t.Fatal("expected error for no nodes")
    }
}

func TestClassifyNode(t *testing.T) {
    tests := []struct {
        node map[string]any
        cls  templateNodeType
    }{
        {map[string]any{"type": "input_rtsp", "role": "source"}, nodeSource},
        {map[string]any{"type": "input_file"}, nodeSource},
        {map[string]any{"type": "preprocess"}, nodePreprocess},
        {map[string]any{"type": "osd"}, nodeOSD},
        {map[string]any{"type": "publish"}, nodePublish},
        {map[string]any{"type": "publish", "role": "sink"}, nodePublish},
        {map[string]any{"type": "alarm"}, nodeAlarm},
        {map[string]any{"type": "ai_yolo"}, nodeDetection},
        {map[string]any{"type": "tracker"}, nodeDetection},
        {map[string]any{"type": "logic_gate"}, nodeDetection},
    }
    for _, tt := range tests {
        got := classifyNode(tt.node)
        if got != tt.cls {
            t.Errorf("classifyNode(%v) = %d, want %d", tt.node, got, tt.cls)
        }
    }
}

func TestSanitizeIDPrefix(t *testing.T) {
    tests := []struct {
        in, want string
    }{
        {"std_face_recognition_stream", "std_face_recognition_stream"},
        {"my-template-v2", "my_template_v2"},
        {"foo.bar", "foo_bar"},
        {"___abc___", "abc"},
    }
    for _, tt := range tests {
        got := sanitizeIDPrefix(tt.in)
        if got != tt.want {
            t.Errorf("sanitizeIDPrefix(%q) = %q, want %q", tt.in, got, tt.want)
        }
    }
}

func TestUniqueID(t *testing.T) {
    used := map[string]bool{"foo": true, "foo_2": true}
    if got := uniqueID("bar", used); got != "bar" {
        t.Errorf("uniqueID(bar) = %s, want bar", got)
    }
    if got := uniqueID("foo", used); got != "foo_3" {
        t.Errorf("uniqueID(foo) = %s, want foo_3", got)
    }
}
