package main

import (
	"os"
	"testing"
	"time"
)

// TestIsCharDeviceRejectsNonConsoles pins down the half of the check that can
// be tested without a terminal: a pipe, a regular file and a closed handle
// must all read as "not a console", because each of them means something other
// than a person waiting at a keyboard.
//
// Getting this wrong in this direction is the dangerous one: it would drop a
// script or a build pipeline into an interactive prompt and hang it forever.
func TestIsCharDeviceRejectsNonConsoles(t *testing.T) {
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	defer r.Close()
	defer w.Close()

	tmp, err := os.CreateTemp(t.TempDir(), "out")
	if err != nil {
		t.Fatalf("temp file: %v", err)
	}
	defer tmp.Close()

	devNull, err := os.Open(os.DevNull)
	if err != nil {
		t.Fatalf("open %s: %v", os.DevNull, err)
	}
	defer devNull.Close()

	for _, tc := range []struct {
		name string
		file *os.File
		want bool
	}{
		{"read end of a pipe", r, false},
		{"write end of a pipe", w, false},
		{"regular file", tmp, false},
		// os.DevNull IS a character device on Unix, so this case documents a
		// real limitation rather than asserting a value that differs by
		// platform: `prog < /dev/null` on Unix looks interactive on stdin.
		// It is harmless, because stdout must ALSO be a console, and a script
		// redirecting stdin almost always redirects stdout too.
	} {
		if got := isCharDevice(tc.file); got != tc.want {
			t.Errorf("isCharDevice(%s) = %v, want %v", tc.name, got, tc.want)
		}
	}
	_ = devNull
}

// TestIsCharDeviceHandlesAClosedFile makes sure a handle we cannot stat is
// treated as "not a console" rather than panicking or defaulting to true.
func TestIsCharDeviceHandlesAClosedFile(t *testing.T) {
	f, err := os.CreateTemp(t.TempDir(), "closed")
	if err != nil {
		t.Fatalf("temp file: %v", err)
	}
	f.Close()
	if isCharDevice(f) {
		t.Error("isCharDevice(closed file) = true, want false")
	}
}

// TestInteractiveConsoleNeedsBothEnds asserts the AND: one console end is not
// enough. Under `go test` stdout is not a terminal, so this must be false.
func TestInteractiveConsoleNeedsBothEnds(t *testing.T) {
	if interactiveConsole() {
		t.Error("interactiveConsole() = true under `go test`, where output is captured; " +
			"the guided prompt would hang any non-interactive run")
	}
}

// TestGuidedRunIsBounded guards the one-keypress default and the ceiling it
// sits under. SensorDeck asks for a run length rather than a folder, so the
// equivalent promise is that the offered answer is usable as-is and that the
// run it starts always ends by itself, soon: whoever double-clicked the
// program has no way to stop it except closing the window.
func TestGuidedRunIsBounded(t *testing.T) {
	if guidedMinMinutes <= 0 {
		t.Errorf("guidedMinMinutes = %v, want a positive length", guidedMinMinutes)
	}
	if guidedMaxMinutes > 10 {
		t.Errorf("guidedMaxMinutes = %v, too long to leave a console window sitting busy", guidedMaxMinutes)
	}
	if guidedMinMinutes > guidedMaxMinutes {
		t.Fatalf("guided range is inverted: %v..%v", guidedMinMinutes, guidedMaxMinutes)
	}
	if guidedDefaultMinutes < guidedMinMinutes || guidedDefaultMinutes > guidedMaxMinutes {
		t.Errorf("guidedDefaultMinutes = %v, outside the range the prompt accepts (%v..%v)",
			guidedDefaultMinutes, guidedMinMinutes, guidedMaxMinutes)
	}
	// A soak needs at least one whole interval to fit inside the run, or the
	// shortest length the prompt offers would be refused outright.
	interval, err := time.ParseDuration(guidedInterval)
	if err != nil {
		t.Fatalf("guidedInterval = %q, which is not a duration: %v", guidedInterval, err)
	}
	shortest := time.Duration(guidedMinMinutes * float64(time.Minute))
	if interval > shortest {
		t.Errorf("guidedInterval %s does not fit inside the shortest guided run (%s)", interval, shortest)
	}
}

// TestParseMinutesRefusesUnusableAnswers checks the retry loop's gate. Every
// rejection has to come back with a sentence for the reader, never an empty
// message and never a Go error string.
func TestParseMinutesRefusesUnusableAnswers(t *testing.T) {
	for _, answer := range []string{"", "soon", "0", "-3", "1e9", "NaN", "999"} {
		got, msg := parseMinutes(answer)
		if msg == "" {
			t.Errorf("parseMinutes(%q) = %v with no explanation, want a refusal", answer, got)
		}
	}
	for _, tc := range []struct {
		answer string
		want   float64
	}{
		{"1", 1}, {"2.5", 2.5}, {" 3 ", 3}, {"2 minutes", 2}, {"4min", 4}, {"5m", 5},
	} {
		got, msg := parseMinutes(tc.answer)
		if msg != "" {
			t.Errorf("parseMinutes(%q) refused a usable answer: %s", tc.answer, msg)
			continue
		}
		if got != tc.want {
			t.Errorf("parseMinutes(%q) = %v, want %v", tc.answer, got, tc.want)
		}
	}
}
