package diag

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/duffy/usb-server/internal/usb"
)

func TestCollectProducesUsableReport(t *testing.T) {
	report := Collect("test")

	if report.Generated == "" {
		t.Error("no timestamp")
	}
	if report.Tool.OS == "" || report.Tool.Arch == "" {
		t.Error("platform not recorded")
	}
	if report.System.Hostname == "" {
		t.Error("hostname not recorded")
	}

	// A report that says nothing about either capability is useless: the
	// whole point is answering whether this machine can share or use.
	if report.Sharing.Mechanism == "" && report.Sharing.Reason == "" {
		t.Error("sharing capability has neither a mechanism nor a reason")
	}
	if report.Using.Mechanism == "" && report.Using.Reason == "" {
		t.Error("using capability has neither a mechanism nor a reason")
	}
}

func TestReportRoundTripsThroughJSON(t *testing.T) {
	report := Collect("test")

	data, err := report.JSON()
	if err != nil {
		t.Fatalf("JSON: %v", err)
	}

	var decoded Report
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("decoding the report we just produced: %v", err)
	}

	if decoded.Tool.OS != report.Tool.OS {
		t.Errorf("OS survived as %q, want %q", decoded.Tool.OS, report.Tool.OS)
	}
	if len(decoded.Devices) != len(report.Devices) {
		t.Errorf("device count changed: %d -> %d", len(report.Devices), len(decoded.Devices))
	}
}

// A failed check without a fix leaves the reader stuck, which defeats the
// purpose of the report.
func TestFailedChecksSuggestAFix(t *testing.T) {
	report := Collect("test")

	for _, check := range report.Checks {
		if !check.Passed && check.Fix == "" && check.Detail == "" {
			t.Errorf("check %q failed but says nothing about why or what to do", check.Name)
		}
	}
}

func TestStringOutputMentionsEverything(t *testing.T) {
	report := &Report{
		Generated: "2026-01-01T00:00:00Z",
		Tool:      ToolInfo{OS: "linux", Arch: "amd64"},
		System:    SystemInfo{Hostname: "testhost"},
		Sharing:   Capability{Available: true, Mechanism: "usbdevfs"},
		Using:     Capability{Available: false, Reason: "vhci-hcd is not loaded"},
		Devices: []DeviceInfo{{
			BusID:     "1-2",
			VendorID:  "046d",
			ProductID: "c52b",
			Name:      "Logitech Receiver",
			Shareable: true,
			Endpoints: []EndpointInfo{{
				Address:      "0x81",
				Direction:    "IN",
				TransferType: "interrupt",
				MaxPacket:    8,
				Interval:     10,
			}},
		}},
		Checks: []Check{
			{Name: "vhci-hcd module", Passed: false,
				Detail: "not loaded", Fix: "sudo modprobe vhci-hcd"},
		},
	}

	out := report.String()

	for _, want := range []string{
		"testhost", "usbdevfs", "vhci-hcd is not loaded",
		"1-2", "046d", "c52b", "Logitech Receiver",
		"0x81", "interrupt",
		"sudo modprobe vhci-hcd",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output does not mention %q", want)
		}
	}
}

func TestTransferTypeNames(t *testing.T) {
	tests := []struct {
		input uint8
		want  string
	}{
		{usb.TransferTypeControl, "control"},
		{usb.TransferTypeIsochronous, "isochronous"},
		{usb.TransferTypeBulk, "bulk"},
		{usb.TransferTypeInterrupt, "interrupt"},
		{99, "unknown(99)"},
	}

	for _, tt := range tests {
		if got := transferTypeName(tt.input); got != tt.want {
			t.Errorf("transferTypeName(%d) = %q, want %q", tt.input, got, tt.want)
		}
	}
}

func TestEndpointInfoReportsDirection(t *testing.T) {
	in := endpointInfo(usb.Endpoint{
		Address:       0x81,
		TransferType:  usb.TransferTypeInterrupt,
		MaxPacketSize: 8,
		Interval:      10,
	})
	if in.Direction != "IN" {
		t.Errorf("0x81 reported as %s, want IN", in.Direction)
	}
	if in.Address != "0x81" {
		t.Errorf("address rendered as %q", in.Address)
	}

	out := endpointInfo(usb.Endpoint{Address: 0x02, TransferType: usb.TransferTypeBulk})
	if out.Direction != "OUT" {
		t.Errorf("0x02 reported as %s, want OUT", out.Direction)
	}
}

func TestDiagURLAcceptsEveryRelayForm(t *testing.T) {
	tests := []struct {
		relay string
		want  string
	}{
		{"ws://relay:8443", "http://relay:8443/diag/abc"},
		{"wss://relay.example.com", "https://relay.example.com/diag/abc"},
		{"http://relay:8443", "http://relay:8443/diag/abc"},
		{"https://relay:8443", "https://relay:8443/diag/abc"},
		{"relay:8443", "http://relay:8443/diag/abc"},
		{"ws://relay:8443/ws", "http://relay:8443/diag/abc"},
		{"ws://relay:8443/", "http://relay:8443/diag/abc"},
	}

	for _, tt := range tests {
		got, err := DiagURL(tt.relay, "abc")
		if err != nil {
			t.Errorf("DiagURL(%q): %v", tt.relay, err)
			continue
		}
		if got != tt.want {
			t.Errorf("DiagURL(%q) = %q, want %q", tt.relay, got, tt.want)
		}
	}
}

func TestDiagURLRejectsBadIDs(t *testing.T) {
	for _, id := range []string{"", "a/b", "a?b", "a#b"} {
		if _, err := DiagURL("ws://relay:8443", id); err == nil {
			t.Errorf("DiagURL accepted the ID %q", id)
		}
	}
}

func TestTruncate(t *testing.T) {
	tests := []struct {
		in   string
		max  int
		want string
	}{
		{"short", 10, "short"},
		{"exactly-10", 10, "exactly-10"},
		{"this is far too long", 10, "this is..."},
		{"abc", 2, "ab"},
	}

	for _, tt := range tests {
		if got := truncate(tt.in, tt.max); got != tt.want {
			t.Errorf("truncate(%q, %d) = %q, want %q", tt.in, tt.max, got, tt.want)
		}
	}
}
