package web

import (
	"context"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/hetznerapi"
	"gitea.dooplex.hu/admin/felhom-hub/internal/offsite"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// disconnectingAPI models the live F1 failure (VALIDATION-offsite-provisioning-e2e 2026-07-09): the
// browser disconnects (impatient re-click) while the subaccount create action is in flight — the request
// context is canceled MID-SEQUENCE. Every call honors the ctx it receives, like the real HTTP client.
type disconnectingAPI struct {
	*hetznerapi.Fake
	cancelReq context.CancelFunc
}

func (a *disconnectingAPI) CreateSubaccount(ctx context.Context, boxID int64, req hetznerapi.CreateSubaccountRequest) (int64, hetznerapi.Action, error) {
	if err := ctx.Err(); err != nil {
		return 0, hetznerapi.Action{}, err
	}
	id, act, err := a.Fake.CreateSubaccount(ctx, boxID, req)
	a.cancelReq() // the client disconnects while the create action runs on Hetzner's side
	return id, act, err
}

func (a *disconnectingAPI) WaitAction(ctx context.Context, act hetznerapi.Action) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	return a.Fake.WaitAction(ctx, act)
}

func (a *disconnectingAPI) GetSubaccount(ctx context.Context, boxID, subID int64) (hetznerapi.Subaccount, error) {
	if err := ctx.Err(); err != nil {
		return hetznerapi.Subaccount{}, err
	}
	return a.Fake.GetSubaccount(ctx, boxID, subID)
}

type webTestScanner struct{}

func (webTestScanner) Fingerprint(_ context.Context, _ string, _ int) (string, error) {
	return "SHA256:webtestfp", nil
}

// Live finding F1: a re-click canceled r.Context() between CreateSubaccount and SaveOneTimeSecret —
// the sub-account was created on Hetzner but its one-time password was lost forever (stranded resource,
// controller consume 404s permanently). applyOffsite must detach provisioning from the client's context:
// once the create starts, create→wait→store runs to completion even if the client goes away.
func TestApplyOffsite_ClientDisconnectMidProvision(t *testing.T) {
	s, st := newTestServer(t)
	reqCtx, cancel := context.WithCancel(context.Background())
	api := &disconnectingAPI{Fake: hetznerapi.NewFake(), cancelReq: cancel}
	s.SetOffsiteProvisioner(&offsite.Provisioner{
		API: api, Store: st, Scanner: webTestScanner{}, PoolBoxID: 611714, Location: "fsn1",
		Logger: log.New(io.Discard, "", 0),
	})

	r := httptest.NewRequest(http.MethodPost,
		"/configs/cust-web/edit?offsite_enabled=on&offsite_type=shared&offsite_quota_gb=50", nil)
	r = r.WithContext(reqCtx)
	// DRTier: offsite requires the DR tier since v0.51.0 (the F-6 coupling) — this test is about
	// the disconnect atom, so the gate is satisfied.
	cfg := &store.CustomerConfig{CustomerID: "cust-web", ConfigJSON: "{}", DRTier: true}

	err := s.applyOffsite(r.Context(), r, cfg)

	if reqCtx.Err() == nil {
		t.Fatal("test harness broken: the request context was never canceled (no disconnect simulated)")
	}
	if err != nil {
		t.Fatalf("provisioning must survive a client disconnect mid-sequence, got: %v", err)
	}
	// The one-time password reached the store — the whole point: no stranded resource with a lost password.
	pw, err := st.ConsumeOneTimeSecret("cust-web")
	if err != nil || pw == "" {
		t.Fatalf("one-time password must be stored despite the disconnect (else the sub-account is stranded): pw=%q err=%v", pw, err)
	}
	// And the descriptor (incl. the pin) was merged for the caller to save.
	if !strings.Contains(cfg.ConfigJSON, `"offsite"`) || !strings.Contains(cfg.ConfigJSON, "SHA256:webtestfp") {
		t.Fatalf("descriptor not merged into ConfigJSON: %s", cfg.ConfigJSON)
	}
}
