package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"go/ast"
	"go/parser"
	"go/token"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestWriteProblemSanitizesAndCorrelates(t *testing.T) {
	var logs bytes.Buffer
	old := problemLogger
	problemLogger = logForTest(&logs)
	t.Cleanup(func() { problemLogger = old })

	const id = "TESTREQUESTID"
	r := httptest.NewRequest(http.MethodPost, "/tools", nil)
	r = r.WithContext(context.WithValue(r.Context(), requestIDKey{}, id))
	w := httptest.NewRecorder()
	internal := errors.New("dial unix /run/private/mavend.sock: bearer secret-token")
	writeProblem(w, r, http.StatusBadGateway, problemToolsChange, "enable failed", internal)

	if w.Code != http.StatusBadGateway {
		t.Fatalf("status = %d, want 502", w.Code)
	}
	if got := w.Header().Get("X-Request-ID"); got != id {
		t.Fatalf("X-Request-ID = %q, want %q", got, id)
	}
	if got := w.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/problem+json") {
		t.Fatalf("Content-Type = %q", got)
	}
	var got problemResponse
	if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode problem: %v", err)
	}
	if got.Error != "enable failed" || got.Code != problemToolsChange || got.RequestID != id {
		t.Fatalf("problem = %+v", got)
	}
	if strings.Contains(w.Body.String(), "private") || strings.Contains(w.Body.String(), "secret-token") {
		t.Fatalf("HTTP response disclosed the wrapped error: %s", w.Body.String())
	}
	for _, want := range []string{id, string(problemToolsChange), internal.Error()} {
		if !strings.Contains(logs.String(), want) {
			t.Errorf("server log missing %q: %s", want, logs.String())
		}
	}
}

func TestRequestIDMiddlewareMintsAndIgnoresCallerID(t *testing.T) {
	var seen string
	h := withRequestID(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seen = requestIDFromContext(r.Context())
		w.WriteHeader(http.StatusNoContent)
	}))
	r := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
	r.Header.Set("X-Request-ID", "caller-chosen")
	w := httptest.NewRecorder()
	h.ServeHTTP(w, r)

	if seen == "" || seen == "caller-chosen" {
		t.Fatalf("request ID = %q; want a server-generated value", seen)
	}
	if got := w.Header().Get("X-Request-ID"); got != seen {
		t.Fatalf("response request ID = %q, context ID = %q", got, seen)
	}
}

func TestEcosystemPanelPropagatesRequestID(t *testing.T) {
	const id = "WEBREQUESTCORRELATION"
	var correlation, requester string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		correlation = r.Header.Get("X-Correlation-ID")
		requester = r.Header.Get("X-Requested-By")
		_, _ = io.WriteString(w, `[]`)
	}))
	defer srv.Close()
	ctx := context.WithValue(context.Background(), requestIDKey{}, id)
	var rows []ecoEntity
	if got := getEco(ctx, srv.URL, "/entities", &rows); got != "" {
		t.Fatalf("getEco error = %q", got)
	}
	if correlation != id || requester != "mavweb" {
		t.Fatalf("correlation = %q, requester = %q", correlation, requester)
	}
}

// The contract is architectural, not a convention people must remember. Keep
// a syntax-level guard so a new handler cannot bypass writeProblem by adding
// another http.Error call.
func TestProductionHandlersUseOneProblemWriter(t *testing.T) {
	entries, err := os.ReadDir(".")
	if err != nil {
		t.Fatal(err)
	}
	for _, entry := range entries {
		name := entry.Name()
		if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
			continue
		}
		file, err := parser.ParseFile(token.NewFileSet(), filepath.Clean(name), nil, 0)
		if err != nil {
			t.Fatalf("parse %s: %v", name, err)
		}
		ast.Inspect(file, func(n ast.Node) bool {
			call, ok := n.(*ast.CallExpr)
			if !ok {
				return true
			}
			sel, ok := call.Fun.(*ast.SelectorExpr)
			if !ok || sel.Sel.Name != "Error" {
				return true
			}
			pkg, ok := sel.X.(*ast.Ident)
			if ok && pkg.Name == "http" {
				t.Errorf("%s contains http.Error; use writeProblem", name)
			}
			return true
		})
	}
}

func logForTest(w io.Writer) *log.Logger {
	return log.New(w, "", 0)
}
