package compose

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

	"github.com/alexei/tinyforge/internal/workload/plugin"
)

func TestComposeProjectName_GivenExplicitValue_PassesThroughVerbatim(t *testing.T) {
	got := composeProjectName("my-explicit-project", plugin.Workload{
		ID:   "abcd1234-5678-1234-abcd-deadbeef0000",
		Name: "WhAtEvEr Name!",
	})
	if got != "my-explicit-project" {
		t.Fatalf("explicit value should win verbatim, got %q", got)
	}
}

func TestComposeProjectName_GivenNoExplicit_DerivesStableSlug(t *testing.T) {
	w := plugin.Workload{
		ID:   "abcd1234-5678-1234-abcd-deadbeef0000",
		Name: "My App",
	}
	got1 := composeProjectName("", w)
	got2 := composeProjectName("", w)

	if got1 != got2 {
		t.Fatalf("derived name must be stable across calls: %q != %q", got1, got2)
	}
	if !strings.HasPrefix(got1, "tf-") {
		t.Fatalf("expected tf- prefix, got %q", got1)
	}
	if !strings.HasSuffix(got1, "-abcd1234") {
		t.Fatalf("expected workload ID short prefix suffix, got %q", got1)
	}
}

func TestComposeProjectName_SanitizesSpecialChars(t *testing.T) {
	cases := []struct {
		name string
		in   string
		// We assert with substring checks so the test does not need to
		// re-implement the full normalization logic.
		mustContain    []string
		mustNotContain []string
	}{
		{
			name:           "spaces become dashes",
			in:             "My App",
			mustContain:    []string{"my-app"},
			mustNotContain: []string{" "},
		},
		{
			name:           "uppercase folded to lowercase",
			in:             "UPPERCASE",
			mustContain:    []string{"uppercase"},
			mustNotContain: []string{"UPPERCASE"},
		},
		{
			name:           "non-alphanum stripped to dash",
			in:             "weird!@#name",
			mustContain:    []string{"weird"},
			mustNotContain: []string{"!", "@", "#"},
		},
		{
			name:           "leading-and-trailing dashes stripped",
			in:             "---name---",
			mustContain:    []string{"name"},
			mustNotContain: []string{"--name", "name--"},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := composeProjectName("", plugin.Workload{
				ID:   "abcd1234-5678-1234-abcd-deadbeef0000",
				Name: tc.in,
			})
			for _, s := range tc.mustContain {
				if !strings.Contains(got, s) {
					t.Errorf("expected %q to contain %q", got, s)
				}
			}
			for _, s := range tc.mustNotContain {
				if strings.Contains(got, s) {
					t.Errorf("expected %q to NOT contain %q", got, s)
				}
			}
		})
	}
}

func TestComposeProjectName_EmptyAfterSanitize_FallsBackToWkl(t *testing.T) {
	got := composeProjectName("", plugin.Workload{
		ID:   "abcd1234-rest",
		Name: "!!!@@@###",
	})
	// All chars get stripped, falls back to "wkl" + ID short prefix.
	if !strings.Contains(got, "wkl") {
		t.Fatalf("expected wkl fallback in %q", got)
	}
	if !strings.HasSuffix(got, "-abcd1234") {
		t.Fatalf("expected ID short prefix suffix in %q", got)
	}
}

func TestComposeProjectName_ShortIDDoesNotPanic(t *testing.T) {
	// IDs shorter than 8 chars must not panic — they are taken verbatim.
	got := composeProjectName("", plugin.Workload{
		ID:   "ab",
		Name: "app",
	})
	if !strings.HasSuffix(got, "-ab") {
		t.Fatalf("short ID handling regressed: %q", got)
	}
}

// withTempDir tries to point os.TempDir() at a per-test scratch
// directory. Note: Go's os.TempDir reads TMPDIR/TMP/TEMP on every call
// (no process-init cache), but isolation across tests rests primarily
// on each test passing a distinct workload-id-derived subdir to
// writeYAML — the env redirect just keeps the directory tree under
// t.TempDir() so the test runner cleans it up at the end. t.Setenv
// restores prior values automatically.
func withTempDir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	t.Setenv("TMPDIR", dir)
	t.Setenv("TMP", dir)
	t.Setenv("TEMP", dir)
	return dir
}

func TestWriteYAML_CreatesFileWithCorrectContents(t *testing.T) {
	withTempDir(t)
	wid := "wid-write-yaml"
	path, err := writeYAML(wid, "services:\n  web:\n    image: nginx:alpine\n")
	if err != nil {
		t.Fatalf("writeYAML: %v", err)
	}
	if filepath.Base(path) != "compose.yml" {
		t.Fatalf("expected compose.yml, got %q", filepath.Base(path))
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read back: %v", err)
	}
	if !strings.Contains(string(body), "nginx:alpine") {
		t.Fatalf("yaml content missing expected line, got %q", string(body))
	}
}

func TestWriteYAMLIfChanged_NoRewriteWhenIdentical(t *testing.T) {
	withTempDir(t)
	wid := "wid-no-rewrite"
	yaml := "services:\n  web:\n    image: nginx:alpine\n"

	path, err := writeYAML(wid, yaml)
	if err != nil {
		t.Fatalf("seed writeYAML: %v", err)
	}
	st1, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}

	// Sleep is unreliable; instead, modify the file then call
	// writeYAMLIfChanged with identical content and assert it did NOT
	// touch the file by checking ModTime is unchanged.
	origMod := st1.ModTime()

	path2, err := writeYAMLIfChanged(wid, yaml)
	if err != nil {
		t.Fatalf("writeYAMLIfChanged: %v", err)
	}
	if path2 != path {
		t.Fatalf("path mismatch: %q vs %q", path, path2)
	}
	st2, err := os.Stat(path2)
	if err != nil {
		t.Fatalf("stat after: %v", err)
	}
	if !st2.ModTime().Equal(origMod) {
		t.Fatalf("file was rewritten despite identical contents (mtime changed: %v -> %v)", origMod, st2.ModTime())
	}
}

func TestWriteYAMLIfChanged_RewritesWhenDifferent(t *testing.T) {
	withTempDir(t)
	wid := "wid-rewrite"
	yaml1 := "services:\n  web:\n    image: nginx:alpine\n"
	yaml2 := "services:\n  web:\n    image: nginx:1.25\n"

	if _, err := writeYAML(wid, yaml1); err != nil {
		t.Fatalf("seed: %v", err)
	}

	path, err := writeYAMLIfChanged(wid, yaml2)
	if err != nil {
		t.Fatalf("writeYAMLIfChanged: %v", err)
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !strings.Contains(string(body), "nginx:1.25") {
		t.Fatalf("yaml not updated, got %q", string(body))
	}
}

func TestWriteYAMLIfChanged_MissingFile_Writes(t *testing.T) {
	withTempDir(t)
	wid := "wid-missing"
	yaml := "services:\n  web:\n    image: alpine\n"

	path, err := writeYAMLIfChanged(wid, yaml)
	if err != nil {
		t.Fatalf("writeYAMLIfChanged: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("expected file to be created, stat err: %v", err)
	}
}

func TestTruncate(t *testing.T) {
	cases := []struct {
		in   string
		n    int
		want string
	}{
		{"short", 100, "short"},
		{"exact", 5, "exact"},
		{"longerstring", 4, "long...(truncated)"},
		{"", 5, ""},
	}
	for _, tc := range cases {
		t.Run(tc.in, func(t *testing.T) {
			if got := truncate(tc.in, tc.n); got != tc.want {
				t.Fatalf("truncate(%q,%d): got %q want %q", tc.in, tc.n, got, tc.want)
			}
		})
	}
}

func TestValidate_GivenValidYAML_Passes(t *testing.T) {
	src := &source{}
	cfg := Config{
		ComposeYAML: "services:\n  web:\n    image: nginx:alpine\n    ports:\n      - \"80\"\n",
	}
	body, _ := json.Marshal(cfg)
	if err := src.Validate(body); err != nil {
		t.Fatalf("expected pass, got %v", err)
	}
}

func TestValidate_RejectsKnownBadInputs(t *testing.T) {
	src := &source{}
	cases := []struct {
		name    string
		body    string
		wantSub string
	}{
		{"empty config", "", "config is required"},
		{"invalid json", `{not json`, "invalid json"},
		{"missing yaml", `{"compose_yaml":""}`, "compose_yaml is required"},
		{"yaml whitespace only", `{"compose_yaml":"   \n  "}`, "compose_yaml is required"},
		{"unparseable yaml", `{"compose_yaml":":\n  bad\n indent"}`, "parse yaml"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := src.Validate([]byte(tc.body))
			if err == nil {
				t.Fatalf("expected error, got nil")
			}
			if !strings.Contains(err.Error(), tc.wantSub) {
				t.Fatalf("error %q missing substring %q", err.Error(), tc.wantSub)
			}
		})
	}
}

func TestKindAndSchemaSample(t *testing.T) {
	src := &source{}
	if src.Kind() != "compose" {
		t.Fatalf("Kind: %q", src.Kind())
	}
	sample := src.SchemaSample()
	cfg, ok := sample.(Config)
	if !ok {
		t.Fatalf("SchemaSample is not Config: %T", sample)
	}
	if cfg.ComposeYAML == "" {
		t.Fatalf("SchemaSample missing ComposeYAML")
	}
}
