package jobspec

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

func TestParseValid(t *testing.T) {
	hcl := `
job "demo" {
}

task "build" {
  command = "/bin/echo"
  args    = ["hello", "world"]
}
`
	spec, err := Parse([]byte(hcl), "test.hcl")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if spec.Job.Name != "demo" {
		t.Errorf("expected job name 'demo', got %q", spec.Job.Name)
	}
	if len(spec.Tasks) != 1 {
		t.Fatalf("expected 1 task, got %d", len(spec.Tasks))
	}
	if spec.Tasks[0].Command != "/bin/echo" {
		t.Errorf("expected command '/bin/echo', got %q", spec.Tasks[0].Command)
	}
	if len(spec.Tasks[0].Args) != 2 {
		t.Errorf("expected 2 args, got %d", len(spec.Tasks[0].Args))
	}
}

func TestParseMissingJob(t *testing.T) {
	hcl := `task "x" { command = "/bin/echo" }`
	_, err := Parse([]byte(hcl), "test.hcl")
	if err == nil {
		t.Fatal("expected error for missing job name")
	}
}

func TestParseNoTasks(t *testing.T) {
	hcl := `job "empty" {}`
	_, err := Parse([]byte(hcl), "test.hcl")
	if err == nil {
		t.Fatal("expected error for no tasks")
	}
}

func TestParseTaskMissingCommand(t *testing.T) {
	hcl := `
job "x" {}
task "no-cmd" {}
`
	_, err := Parse([]byte(hcl), "test.hcl")
	if err == nil {
		t.Fatal("expected error for missing command")
	}
}

func TestParse_GoldenFiles(t *testing.T) {
	cases := []struct {
		name        string
		file        string
		wantJob     string
		wantJobType string
		wantTasks   int
		checkTask   func(t *testing.T, s *Spec)
	}{
		{
			name:        "single_task",
			file:        "valid_single_task.hcl",
			wantJob:     "single",
			wantTasks:   1,
			wantJobType: "",
			checkTask: func(t *testing.T, s *Spec) {
				if s.Tasks[0].Name != "solo" {
					t.Errorf("task name = %q, want solo", s.Tasks[0].Name)
				}
				if s.Tasks[0].Command != "/bin/true" {
					t.Errorf("command = %q, want /bin/true", s.Tasks[0].Command)
				}
			},
		},
		{
			name:        "multi_task",
			file:        "valid_multi_task.hcl",
			wantJob:     "multi",
			wantJobType: "batch",
			wantTasks:   3,
			checkTask: func(t *testing.T, s *Spec) {
				byName := map[string]TaskSpec{}
				for _, tk := range s.Tasks {
					byName[tk.Name] = tk
				}
				if _, ok := byName["build"]; !ok {
					t.Errorf("missing task 'build'")
				}
				if _, ok := byName["test"]; !ok {
					t.Errorf("missing task 'test'")
				}
				if len(byName["test"].Env) != 2 {
					t.Errorf("test env count = %d, want 2", len(byName["test"].Env))
				}
				if _, ok := byName["deploy"]; !ok {
					t.Errorf("missing task 'deploy'")
				}
			},
		},
		{
			name:      "env_vars",
			file:      "valid_env_vars.hcl",
			wantJob:   "envvars",
			wantTasks: 1,
			checkTask: func(t *testing.T, s *Spec) {
				if len(s.Tasks[0].Env) != 3 {
					t.Errorf("env count = %d, want 3", len(s.Tasks[0].Env))
				}
				want := "FOO=bar"
				if s.Tasks[0].Env[0] != want {
					t.Errorf("env[0] = %q, want %q", s.Tasks[0].Env[0], want)
				}
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			path := filepath.Join("testdata", tc.file)
			spec, err := ParseHCLFile(path)
			if err != nil {
				t.Fatalf("ParseHCLFile(%s): %v", tc.file, err)
			}
			if spec.Job.Name != tc.wantJob {
				t.Errorf("job name = %q, want %q", spec.Job.Name, tc.wantJob)
			}
			if tc.wantJobType != "" && spec.Job.Type != tc.wantJobType {
				t.Errorf("job type = %q, want %q", spec.Job.Type, tc.wantJobType)
			}
			if len(spec.Tasks) != tc.wantTasks {
				t.Fatalf("tasks = %d, want %d", len(spec.Tasks), tc.wantTasks)
			}
			if tc.checkTask != nil {
				tc.checkTask(t, spec)
			}
		})
	}
}

func TestParse_ErrorPaths(t *testing.T) {
	cases := []struct {
		name     string
		file     string
		wantErr  string
		useParse bool
		hcl      string
	}{
		{name: "no_tasks", file: "err_no_tasks.hcl", wantErr: "at least one task"},
		{name: "missing_command", file: "err_missing_command.hcl", wantErr: "required"},
		{name: "malformed", file: "err_malformed.hcl", wantErr: "decode hcl"},
		{name: "missing_job", file: "err_missing_job.hcl", wantErr: "Missing job block"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			path := filepath.Join("testdata", tc.file)
			_, err := ParseFile(path)
			if err == nil {
				t.Fatalf("expected error containing %q, got nil", tc.wantErr)
			}
			if !strings.Contains(err.Error(), tc.wantErr) {
				t.Errorf("error = %q, want it to contain %q", err.Error(), tc.wantErr)
			}
		})
	}
}

func TestParse_EmptyFile(t *testing.T) {
	_, err := Parse([]byte(""), "empty.hcl")
	if err == nil {
		t.Fatal("expected error for empty file")
	}
}

func TestParse_MalformedHCL(t *testing.T) {
	_, err := Parse([]byte("job = "), "bad.hcl")
	if err == nil {
		t.Fatal("expected error for malformed HCL")
	}
	if !strings.Contains(err.Error(), "decode hcl") {
		t.Errorf("error = %q, want it to contain 'decode hcl'", err.Error())
	}
}

func TestParseFile_Nonexistent(t *testing.T) {
	_, err := ParseFile(filepath.Join("testdata", "does_not_exist.hcl"))
	if err == nil {
		t.Fatal("expected error for nonexistent file")
	}
	if !strings.Contains(err.Error(), "read spec file") {
		t.Errorf("error = %q, want it to contain 'read spec file'", err.Error())
	}
}

func TestParseFile_ReadError(t *testing.T) {
	// Directory exists but is not readable as a file.
	_, err := ParseFile("testdata")
	if err == nil {
		t.Fatal("expected error when ParseFile target is a directory")
	}
}

func TestSpec_Validate(t *testing.T) {
	cases := []struct {
		name    string
		spec    *Spec
		wantErr string
	}{
		{
			name:    "empty_job_name",
			spec:    &Spec{Job: JobSpec{Name: "   "}, Tasks: []TaskSpec{{Name: "t", Command: "/bin/echo"}}},
			wantErr: "job name is required",
		},
		{
			name:    "no_tasks",
			spec:    &Spec{Job: JobSpec{Name: "x"}},
			wantErr: "at least one task is required",
		},
		{
			name: "valid",
			spec: &Spec{Job: JobSpec{Name: "x"}, Tasks: []TaskSpec{{Name: "t", Command: "/bin/echo"}}},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.spec.Validate()
			if tc.wantErr == "" {
				if err != nil {
					t.Errorf("Validate: got %v, want nil", err)
				}
				return
			}
			if err == nil {
				t.Fatalf("expected error containing %q, got nil", tc.wantErr)
			}
			if !strings.Contains(err.Error(), tc.wantErr) {
				t.Errorf("error = %q, want it to contain %q", err.Error(), tc.wantErr)
			}
		})
	}
}

func TestSpec_Validate_RoundTripFromParse(t *testing.T) {
	path := filepath.Join("testdata", "valid_single_task.hcl")
	spec, err := ParseHCLFile(path)
	if err != nil {
		t.Fatalf("ParseHCLFile: %v", err)
	}
	if err := spec.Validate(); err != nil {
		t.Errorf("Validate on parsed spec: %v", err)
	}
}

func TestParseFile_GoldenFilesExist(t *testing.T) {
	// Guard against accidentally removing testdata fixtures.
	files := []string{
		"valid_single_task.hcl",
		"valid_multi_task.hcl",
		"valid_env_vars.hcl",
		"err_no_tasks.hcl",
		"err_missing_command.hcl",
		"err_malformed.hcl",
		"err_missing_job.hcl",
	}
	for _, f := range files {
		path := filepath.Join("testdata", f)
		if _, err := os.Stat(path); err != nil {
			t.Errorf("missing testdata fixture %s: %v", f, err)
		}
	}
}
