package template

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"text/template"

	"filippo.io/age"

	vaultpkg "github.com/danieljustus/symaira-vault/internal/vault"
)

func newTestVault(t *testing.T, entries map[string]map[string]any) *vaultpkg.Vault {
	t.Helper()
	vaultDir := t.TempDir()
	id, err := age.GenerateX25519Identity()
	if err != nil {
		t.Fatalf("generate identity: %v", err)
	}
	for path, data := range entries {
		if err := vaultpkg.WriteEntry(vaultDir, path, &vaultpkg.Entry{Data: data}, id); err != nil {
			t.Fatalf("WriteEntry(%s): %v", path, err)
		}
	}
	return &vaultpkg.Vault{Dir: vaultDir, Identity: id}
}

// newMockVault creates a test vault with entries that map to path/field-value responses.
// For a path/field pair, the entry data will contain field with value "path/field-value".
func newMockVault(t *testing.T) *vaultpkg.Vault {
	t.Helper()
	entries := map[string]map[string]any{
		"prod/api": {"key": "prod/api/key-value"},
		"prod/db":  {"password": "prod/db/password-value"},
		"path":     {"field": "path/field-value"},
	}
	return newTestVault(t, entries)
}

func TestNewEngine(t *testing.T) {
	mock := newMockVault(t)
	eng := NewEngine(mock)
	if eng == nil {
		t.Fatal("NewEngine returned nil")
	}
	if eng.vault != mock {
		t.Error("engine vault mismatch")
	}
	if len(eng.builtins) != 5 {
		t.Fatalf("expected 5 builtins, got %d", len(eng.builtins))
	}
}

func TestListBuiltins(t *testing.T) {
	eng := NewEngine(newMockVault(t))
	got := eng.ListBuiltins()
	want := []string{"docker-compose", "env", "github-actions", "k8s-secret", "terraform"}
	if !slices.Equal(got, want) {
		t.Fatalf("ListBuiltins() = %v, want %v", got, want)
	}
}

func TestRender(t *testing.T) {
	ctx := context.Background()
	eng := NewEngine(newMockVault(t))

	tests := []struct {
		name         string
		templateName string
		resourceName string
		refs         map[string]string
		dryRun       bool
		wantContains []string
		wantErr      bool
	}{
		{
			name:         "env template",
			templateName: "env",
			resourceName: "app",
			refs:         map[string]string{"API_KEY": "op://prod/api/key", "DB_PASS": "prod/db.password"},
			dryRun:       false,
			wantContains: []string{"export API_KEY=", "export DB_PASS="},
			wantErr:      false,
		},
		{
			name:         "k8s-secret template",
			templateName: "k8s-secret",
			resourceName: "app-secrets",
			refs:         map[string]string{"API_KEY": "op://prod/api/key"},
			dryRun:       false,
			wantContains: []string{"apiVersion: v1", "kind: Secret", "name: app-secrets"},
			wantErr:      false,
		},
		{
			name:         "docker-compose template",
			templateName: "docker-compose",
			resourceName: "app",
			refs:         map[string]string{"API_KEY": "op://prod/api/key"},
			dryRun:       false,
			wantContains: []string{"version: \"3.8\"", "services:", "secrets:"},
			wantErr:      false,
		},
		{
			name:         "github-actions template",
			templateName: "github-actions",
			resourceName: "deploy",
			refs:         map[string]string{"API_KEY": "op://prod/api/key"},
			dryRun:       false,
			wantContains: []string{"name: Deploy deploy", "runs-on: ubuntu-latest"},
			wantErr:      false,
		},
		{
			name:         "terraform template",
			templateName: "terraform",
			resourceName: "infra",
			refs:         map[string]string{"API_KEY": "op://prod/api/key"},
			dryRun:       false,
			wantContains: []string{"variable \"api_key\"", "sensitive = true"},
			wantErr:      false,
		},
		{
			name:         "dry run masks values",
			templateName: "env",
			resourceName: "app",
			refs:         map[string]string{"API_KEY": "op://prod/api/key"},
			dryRun:       true,
			wantContains: []string{"***"},
			wantErr:      false,
		},
		{
			name:         "unknown template",
			templateName: "unknown",
			resourceName: "app",
			refs:         map[string]string{},
			dryRun:       false,
			wantContains: nil,
			wantErr:      true,
		},
		{
			name:         "empty refs",
			templateName: "env",
			resourceName: "empty",
			refs:         map[string]string{},
			dryRun:       false,
			wantContains: []string{"# Generated by Symaira Vault"},
			wantErr:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := eng.Render(ctx, tt.templateName, tt.resourceName, tt.refs, tt.dryRun)
			if tt.wantErr {
				if err == nil {
					t.Fatalf("Render() expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("Render() unexpected error: %v", err)
			}
			for _, want := range tt.wantContains {
				if !strings.Contains(got, want) {
					t.Errorf("Render() output missing %q\ngot:\n%s", want, got)
				}
			}
		})
	}
}

func TestRenderCustomTemplate(t *testing.T) {
	ctx := context.Background()
	eng := NewEngine(newMockVault(t))

	dir := t.TempDir()
	customTmpl := `custom: {{ .Name }}
{{- range $key, $ref := .Refs }}
{{ $key }}={{ index $.Values $key }}
{{- end }}`
	if err := os.WriteFile(filepath.Join(dir, "custom.tmpl"), []byte(customTmpl), 0644); err != nil {
		t.Fatalf("write custom template: %v", err)
	}

	if err := eng.LoadCustomTemplates(dir); err != nil {
		t.Fatalf("LoadCustomTemplates: %v", err)
	}

	got, err := eng.Render(ctx, "custom", "test", map[string]string{"KEY": "op://path/field"}, false)
	if err != nil {
		t.Fatalf("Render custom template: %v", err)
	}
	if !strings.Contains(got, "custom: test") {
		t.Errorf("custom template missing resource name: %s", got)
	}
}

func TestValidate(t *testing.T) {
	ctx := context.Background()
	vault := newTestVault(t, map[string]map[string]any{
		"prod/api": {"key": "value"},
	})
	eng := NewEngine(vault)

	tests := []struct {
		name    string
		refs    map[string]string
		wantErr bool
	}{
		{
			name:    "all refs valid",
			refs:    map[string]string{"KEY": "op://prod/api/key"},
			wantErr: false,
		},
		{
			name:    "missing entry",
			refs:    map[string]string{"KEY": "op://missing/field"},
			wantErr: true,
		},
		{
			name:    "empty refs",
			refs:    map[string]string{},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := eng.Validate(ctx, "env", tt.refs)
			if tt.wantErr {
				if err == nil {
					t.Fatal("Validate() expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("Validate() unexpected error: %v", err)
			}
		})
	}
}

func TestLoadCustomTemplates(t *testing.T) {
	eng := NewEngine(newMockVault(t))

	t.Run("valid directory", func(t *testing.T) {
		dir := t.TempDir()
		if err := os.WriteFile(filepath.Join(dir, "foo.tmpl"), []byte("foo"), 0644); err != nil {
			t.Fatalf("write template: %v", err)
		}
		if err := os.WriteFile(filepath.Join(dir, "bar.tmpl"), []byte("bar"), 0644); err != nil {
			t.Fatalf("write template: %v", err)
		}
		if err := eng.LoadCustomTemplates(dir); err != nil {
			t.Fatalf("LoadCustomTemplates: %v", err)
		}

		foo, err := eng.getTemplateSource("foo")
		if err != nil {
			t.Fatalf("getTemplateSource(foo): %v", err)
		}
		if foo != "foo" {
			t.Errorf("foo template = %q, want %q", foo, "foo")
		}

		bar, err := eng.getTemplateSource("bar")
		if err != nil {
			t.Fatalf("getTemplateSource(bar): %v", err)
		}
		if bar != "bar" {
			t.Errorf("bar template = %q, want %q", bar, "bar")
		}
	})

	t.Run("non-existent directory", func(t *testing.T) {
		err := eng.LoadCustomTemplates("/does/not/exist")
		if err == nil {
			t.Fatal("expected error for non-existent directory")
		}
	})

	t.Run("ignores non-tmpl files", func(t *testing.T) {
		dir := t.TempDir()
		if err := os.WriteFile(filepath.Join(dir, "readme.txt"), []byte("readme"), 0644); err != nil {
			t.Fatalf("write file: %v", err)
		}
		if err := eng.LoadCustomTemplates(dir); err != nil {
			t.Fatalf("LoadCustomTemplates: %v", err)
		}
		_, err := eng.getTemplateSource("readme")
		if err == nil {
			t.Fatal("expected error for non-tmpl file")
		}
	})
}

func TestGetTemplateSource(t *testing.T) {
	eng := NewEngine(newMockVault(t))

	t.Run("builtin", func(t *testing.T) {
		src, err := eng.getTemplateSource("env")
		if err != nil {
			t.Fatalf("getTemplateSource(env): %v", err)
		}
		if src == "" {
			t.Error("env template is empty")
		}
	})

	t.Run("custom overrides builtin", func(t *testing.T) {
		eng.custom["env"] = "custom-env"
		src, err := eng.getTemplateSource("env")
		if err != nil {
			t.Fatalf("getTemplateSource(env): %v", err)
		}
		if src != "custom-env" {
			t.Errorf("custom override = %q, want %q", src, "custom-env")
		}
	})

	t.Run("not found", func(t *testing.T) {
		_, err := eng.getTemplateSource("nonexistent")
		if !errors.Is(err, ErrTemplateNotFound) {
			t.Fatalf("expected ErrTemplateNotFound, got %v", err)
		}
	})
}

func TestBuiltinTemplatesAreValid(t *testing.T) {
	funcs := DefaultFuncMap()
	for name, src := range builtinTemplates() {
		t.Run(name, func(t *testing.T) {
			_, err := template.New(name).Funcs(funcs).Parse(src)
			if err != nil {
				t.Fatalf("template %q failed to parse: %v", name, err)
			}
		})
	}
}

func TestRenderDataStructure(t *testing.T) {
	data := RenderData{
		Name:   "test",
		Refs:   map[string]string{"A": "op://p/f"},
		Values: map[string]string{"A": "val"},
	}
	if data.Name != "test" {
		t.Error("Name mismatch")
	}
	if data.Refs["A"] != "op://p/f" {
		t.Error("Refs mismatch")
	}
	if data.Values["A"] != "val" {
		t.Error("Values mismatch")
	}
}
