package web

import (
	"encoding/json"
	"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"
)

// postForm builds a POST request whose urlencoded body is parsed by r.FormValue/r.ParseForm,
// exactly like the real edit submission.
func postForm(target, body string) *http.Request {
	r := httptest.NewRequest(http.MethodPost, target, strings.NewReader(body))
	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	return r
}

// The debug-mode toggle is a FORM field (not raw-JSON injection) so it survives the on-save rebuild.
// This asserts the form→JSON leg: checked writes logging.level=debug; unchecked omits the key entirely
// (so the generated controller.yaml default stands — no needless "info").
func TestBuildConfigJSON_DebugMode(t *testing.T) {
	// Checked → logging.level=debug present.
	got := buildConfigJSON(postForm("/configs/x/edit", "debug_mode=on"))
	var on map[string]interface{}
	if err := json.Unmarshal([]byte(got), &on); err != nil {
		t.Fatalf("checked: bad JSON %q: %v", got, err)
	}
	logging, ok := on["logging"].(map[string]interface{})
	if !ok {
		t.Fatalf("checked: expected a logging object, got %q", got)
	}
	if logging["level"] != "debug" {
		t.Fatalf("checked: expected logging.level=debug, got %v (%q)", logging["level"], got)
	}

	// Unchecked → NO logging key at all (not level=info).
	got = buildConfigJSON(postForm("/configs/x/edit", "customer_name=Kov%C3%A1cs"))
	var off map[string]interface{}
	if err := json.Unmarshal([]byte(got), &off); err != nil {
		t.Fatalf("unchecked: bad JSON %q: %v", got, err)
	}
	if _, present := off["logging"]; present {
		t.Fatalf("unchecked: logging key must be OMITTED, got %q", got)
	}
}

// The whole point of the form-level toggle: it survives the on-save ConfigJSON REBUILD, and coexists
// with the offsite descriptor (which survives via the separate provision-merge). Companion RED-PROOF:
// a foreign key injected straight into the stored ConfigJSON is GONE after one save — which is exactly
// why the switch had to be a form field, not raw-JSON injection.
func TestConfigUpdate_DebugSurvivesRebuild_OffsiteUntouched(t *testing.T) {
	s, st := newTestServer(t)
	s.SetOffsiteProvisioner(&offsite.Provisioner{
		API: hetznerapi.NewFake(), Store: st, Scanner: webTestScanner{},
		PoolBoxID: 611714, Location: "fsn1", Logger: log.New(io.Discard, "", 0),
	})

	const id = "cust-dbg"
	if err := st.SaveCustomerConfig(&store.CustomerConfig{
		CustomerID: id, CustomerName: "Kovács", Domain: "kovacs.felhom.eu", ConfigJSON: "{}",
	}); err != nil {
		t.Fatalf("seed customer: %v", err)
	}

	// 1) First save WITH offsite enabled → provisions + merges the descriptor. No debug yet.
	const offsiteForm = "customer_name=Kov%C3%A1cs&domain=kovacs.felhom.eu&dr_tier=on&offsite_enabled=on&offsite_type=shared&offsite_quota_gb=50"
	w := httptest.NewRecorder()
	s.handleConfigUpdate(w, postForm("/configs/"+id+"/edit", offsiteForm), id)
	if w.Code != http.StatusSeeOther {
		t.Fatalf("first save: expected 303, got %d: %s", w.Code, w.Body.String())
	}
	cfg, _ := st.GetCustomerConfig(id)
	before := offsiteOf(t, cfg.ConfigJSON)
	if before["host"] == nil || before["host"] == "" {
		t.Fatalf("first save should have merged an offsite descriptor, got %q", cfg.ConfigJSON)
	}

	// 2) Inject a foreign key directly into the stored ConfigJSON — the raw-JSON path the task ruled out.
	var obj map[string]json.RawMessage
	json.Unmarshal([]byte(cfg.ConfigJSON), &obj)
	obj["foo"] = json.RawMessage(`"bar"`)
	inj, _ := json.Marshal(obj)
	cfg.ConfigJSON = string(inj)
	if err := st.SaveCustomerConfig(cfg); err != nil {
		t.Fatalf("inject foreign key: %v", err)
	}

	// 3) Second save WITH offsite still enabled AND debug checked.
	w = httptest.NewRecorder()
	s.handleConfigUpdate(w, postForm("/configs/"+id+"/edit", offsiteForm+"&debug_mode=on"), id)
	if w.Code != http.StatusSeeOther {
		t.Fatalf("second save: expected 303, got %d: %s", w.Code, w.Body.String())
	}
	cfg, _ = st.GetCustomerConfig(id)

	var final map[string]interface{}
	if err := json.Unmarshal([]byte(cfg.ConfigJSON), &final); err != nil {
		t.Fatalf("final ConfigJSON invalid: %v", err)
	}

	// (a) debug logging key made it in.
	logging, _ := final["logging"].(map[string]interface{})
	if logging == nil || logging["level"] != "debug" {
		t.Fatalf("debug toggle did not survive the save: logging=%v (%q)", final["logging"], cfg.ConfigJSON)
	}

	// (b) offsite descriptor UNCHANGED across the save+re-provision (idempotent label lookup).
	after := offsiteOf(t, cfg.ConfigJSON)
	for _, k := range []string{"host", "user", "repo_path", "host_fingerprint", "quota_gb"} {
		if before[k] != after[k] {
			t.Fatalf("offsite.%s changed across save: %v -> %v", k, before[k], after[k])
		}
	}

	// RED-PROOF: the injected foreign key is GONE — buildConfigJSON rebuilt ConfigJSON from the form,
	// so anything not represented as a form field is dropped. This is why the switch is a form field.
	if _, present := final["foo"]; present {
		t.Fatalf("foreign key survived the rebuild — the on-save rebuild premise is wrong: %q", cfg.ConfigJSON)
	}
}

// offsiteOf extracts the offsite sub-object as a generic map for field-by-field comparison.
func offsiteOf(t *testing.T, configJSON string) map[string]interface{} {
	t.Helper()
	var m map[string]interface{}
	if err := json.Unmarshal([]byte(configJSON), &m); err != nil {
		t.Fatalf("parse ConfigJSON: %v", err)
	}
	o, _ := m["offsite"].(map[string]interface{})
	if o == nil {
		t.Fatalf("no offsite object in %q", configJSON)
	}
	return o
}

// Render leg: a debug-on ConfigJSON draws the checkbox checked; a plain one draws it unchecked.
func TestConfigForm_DebugRenderState(t *testing.T) {
	s, _ := newTestServer(t)

	render := func(configJSON string) string {
		var overrides map[string]interface{}
		json.Unmarshal([]byte(configJSON), &overrides)
		data := struct {
			IsNew     bool
			Config    *store.CustomerConfig
			Overrides map[string]interface{}
			ActiveNav string
			Error     string
			CSRFField string
			PBSDR     pbsDRView
		}{
			Config:    &store.CustomerConfig{CustomerID: "c1"},
			Overrides: overrides,
			ActiveNav: "configs",
		}
		var b strings.Builder
		if err := s.templates.ExecuteTemplate(&b, "config_form.html", data); err != nil {
			t.Fatalf("render: %v", err)
		}
		return b.String()
	}

	// debug on → the debug_mode checkbox carries `checked`.
	out := render(`{"logging":{"level":"debug"}}`)
	if !debugChecked(out) {
		t.Fatalf("debug ConfigJSON should render the checkbox checked:\n%s", isolate(out))
	}
	// plain → not checked.
	out = render(`{}`)
	if debugChecked(out) {
		t.Fatalf("plain ConfigJSON must render the checkbox UNchecked:\n%s", isolate(out))
	}
}

// debugChecked reports whether the debug_mode checkbox input renders with the `checked` attribute.
func debugChecked(html string) bool {
	i := strings.Index(html, `name="debug_mode"`)
	if i < 0 {
		return false
	}
	// The input tag ends at the next '>'; `checked` (if present) sits before it.
	end := strings.Index(html[i:], ">")
	if end < 0 {
		return false
	}
	return strings.Contains(html[i:i+end], "checked")
}

// isolate trims the rendered page down to the debug_mode input's neighborhood for readable failures.
func isolate(html string) string {
	i := strings.Index(html, `name="debug_mode"`)
	if i < 0 {
		return "(debug_mode input not found)"
	}
	start := i - 60
	if start < 0 {
		start = 0
	}
	end := i + 60
	if end > len(html) {
		end = len(html)
	}
	return html[start:end]
}
