package main

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/phraser"
)

// A refused credential and an outage are different answers, and on the Hexis
// path only one of them used to be said. These tests pin the difference at both
// Hexis sites: the discovery hop and the execute hop (Vikunja #587). The Praxis
// half of the same defect is in praxis_gap_test.go.
//
// unreachableURL is a port nothing listens on, which is what "the service is
// down" looks like from inside a call: the connection is refused, no HTTP
// answer is ever produced, and ecosystemError.Unreachable() is true.
const unreachableURL = "http://127.0.0.1:1"

func denied(service, reply string) bool {
	return phraser.IsA(phraser.EcoDenied, serviceVars(service), reply)
}

func down(service, reply string) bool {
	return phraser.IsA(phraser.EcoDown, serviceVars(service), reply)
}

// hexisGapHandler wires a handler whose Nexus resolves cleanly and whose Hexis
// is the caller's to break. hexisURL is taken separately so a test can point it
// at a dead port.
func hexisGapHandler(t *testing.T, nexusURL, hexisURL string) *reactiveHandler {
	t.Helper()
	st := newTestStore(t)
	now := time.Now()
	return &reactiveHandler{
		api:       ipc.NewStoreAPI(st),
		dataStore: st,
		now:       func() time.Time { return now },
		ecosystem: stubEcosystem(nexusURL, hexisURL),
	}
}

// TestHexisDiscovery401IsDeniedNotDown — the discovery hop.
//
// The vendored Hexis client returns a plain fmt.Errorf for every status at or
// above 400, so errors.As for *ecosystemError never matched and every failure
// fell through to the outage line. "Hexis is down" for a rejected token sends
// him to inspect a service that is running fine.
func TestHexisDiscovery401IsDeniedNotDown(t *testing.T) {
	ctx := context.Background()
	nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", muzickIndexer, "service"))
	caps := fixtureHexisCapabilities(map[string]any{"id": "cap_restart", "name": "restart", "read_only": true})
	hexis := newFakeHexis(t, caps, fixtureHexisExecuted("exec_1", "succeeded"))
	h := hexisGapHandler(t, nexus.URL, hexis.URL)

	hexis.SetFault(401)
	reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
	if !denied(serviceHexis, reply) {
		t.Fatalf("401 from hexis discovery: got %q, want the denied line naming Hexis", reply)
	}
	if !strings.Contains(reply, serviceHexis) {
		t.Errorf("reply does not name Hexis: %q", reply)
	}
}

// TestHexisDiscoveryOutageIsDownNotDenied — the other half of the same fork.
// Without this the fix could pass by calling everything a refused credential.
func TestHexisDiscoveryOutageIsDownNotDenied(t *testing.T) {
	ctx := context.Background()
	nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", muzickIndexer, "service"))
	h := hexisGapHandler(t, nexus.URL, unreachableURL)

	reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
	if !down(serviceHexis, reply) {
		t.Fatalf("connection refused from hexis: got %q, want the outage line naming Hexis", reply)
	}
	if denied(serviceHexis, reply) {
		t.Error("an outage must not be reported as a refused credential")
	}
}

// TestHexisExecute401IsDeniedNotCommandFailure — the execute hop, which did not
// consult ecosystemGap at all and named neither the service nor the cause.
func TestHexisExecute401IsDeniedNotCommandFailure(t *testing.T) {
	ctx := context.Background()
	nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", muzickIndexer, "service"))
	caps := fixtureHexisCapabilities(map[string]any{"id": "cap_restart", "name": "restart", "read_only": true})
	hexis := newFakeHexis(t, caps, fixtureHexisExecuted("exec_1", "succeeded"))
	h := hexisGapHandler(t, nexus.URL, hexis.URL)

	// Discovery stays healthy; only the execute endpoint refuses. A blanket
	// fault would never reach the site under test.
	hexis.SetRouteFault("/api/v1/execute", 401)
	reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
	if !denied(serviceHexis, reply) {
		t.Fatalf("401 from hexis execute: got %q, want the denied line naming Hexis", reply)
	}
}

// TestHexisExecuteOutageIsDown — same site, the other classification.
//
// Discovery and execution share one base URL, so the outage has to be scoped to
// the execute endpoint rather than to the server: it answers capabilities
// normally and drops the connection on execute, which is what the client sees
// when the far side dies mid-call. That produces no HTTP status at all, which is
// what Unreachable() means.
func TestHexisExecuteOutageIsDown(t *testing.T) {
	ctx := context.Background()
	nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", muzickIndexer, "service"))
	caps := fixtureHexisCapabilities(map[string]any{"id": "cap_restart", "name": "restart", "read_only": true})
	hexis := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/v1/execute" {
			conn, _, err := w.(http.Hijacker).Hijack()
			if err != nil {
				t.Errorf("hijack: %v", err)
				return
			}
			conn.Close()
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(caps))
	}))
	t.Cleanup(hexis.Close)
	h := hexisGapHandler(t, nexus.URL, hexis.URL)

	reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
	if !down(serviceHexis, reply) {
		t.Fatalf("dropped connection on hexis execute: got %q, want the outage line", reply)
	}
	if denied(serviceHexis, reply) {
		t.Error("an outage must not be reported as a refused credential")
	}
}

// TestHexisExecutionFailedStaysCommandFailure — the boundary of the fix. Hexis
// answering 200 with a failed execution is the command failing, not Hexis
// degrading, and it must keep the command-level line rather than accusing a
// healthy service of being down.
func TestHexisExecutionFailedStaysCommandFailure(t *testing.T) {
	ctx := context.Background()
	nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", muzickIndexer, "service"))
	caps := fixtureHexisCapabilities(map[string]any{"id": "cap_restart", "name": "restart", "read_only": true})
	hexis := newFakeHexis(t, caps, fixtureHexisExecutionFailed("exec_1", "unit refused to start"))
	h := hexisGapHandler(t, nexus.URL, hexis.URL)

	reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
	if down(serviceHexis, reply) || denied(serviceHexis, reply) {
		t.Fatalf("a failed execution must not be reported as an ecosystem gap, got %q", reply)
	}
	if !phraser.IsA(phraser.ActFailEntity, map[string]string{"name": muzickIndexer}, reply) {
		t.Fatalf("want the command-failure line, got %q", reply)
	}
}
