package api_test

import (
	"encoding/json"
	"io"
	"net/http"
	"testing"
)

// These tests exercise the many handlers reachable through the default setup()
// harness, whose policy is unconfigured (anonymous caller → write/admin), so
// permission gates default to "allow" and the focus is handler behaviour.

// ── /me ───────────────────────────────────────────────────────────────────────

func TestGetMe(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := get(t, srv, "/api/v1/me")
	assertStatus(t, resp, http.StatusOK)
	var me map[string]any
	readJSON(t, resp, &me)
	for _, k := range []string{"user", "level", "groups", "canEditLogic", "canViewAudit", "canAdmin", "defaultZoom"} {
		if _, ok := me[k]; !ok {
			t.Errorf("/me missing key %q", k)
		}
	}
	// Unconfigured policy → anonymous caller has write level.
	if me["level"] != "write" {
		t.Errorf("level = %v, want write", me["level"])
	}
}

// ── /groups (signal group tree) ───────────────────────────────────────────────

func TestGroupsRoundTrip(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	// Initially valid JSON.
	resp := get(t, srv, "/api/v1/groups")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Replace with a JSON array → 204.
	resp = putRaw(t, srv, "/api/v1/groups", "application/json", []byte(`[{"name":"A"},{"name":"B"}]`))
	assertStatus(t, resp, http.StatusNoContent)

	// Read back what we wrote.
	resp = get(t, srv, "/api/v1/groups")
	assertStatus(t, resp, http.StatusOK)
	body, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	var tree []map[string]any
	if err := json.Unmarshal(body, &tree); err != nil {
		t.Fatalf("groups body not a JSON array: %v (%s)", err, body)
	}
	if len(tree) != 2 {
		t.Fatalf("expected 2 groups, got %d", len(tree))
	}

	// A non-array body is rejected.
	resp = putRaw(t, srv, "/api/v1/groups", "application/json", []byte(`{"not":"array"}`))
	assertStatus(t, resp, http.StatusBadRequest)
}

// ── /usergroups ───────────────────────────────────────────────────────────────

func TestListUserGroups(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := get(t, srv, "/api/v1/usergroups")
	assertStatus(t, resp, http.StatusOK)
	var names []string
	readJSON(t, resp, &names)
	// Unconfigured policy has no named groups; the handler must still return [].
	if names == nil {
		t.Error("expected non-nil (possibly empty) group list")
	}
}

// ── /folders ──────────────────────────────────────────────────────────────────

func TestFolderCRUD(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	// List — initially empty.
	resp := get(t, srv, "/api/v1/folders")
	assertStatus(t, resp, http.StatusOK)
	var list []any
	readJSON(t, resp, &list)
	if len(list) != 0 {
		t.Fatalf("expected no folders, got %d", len(list))
	}

	// Missing name → 400.
	resp = postJSON(t, srv, "/api/v1/folders", map[string]any{"name": ""})
	assertStatus(t, resp, http.StatusBadRequest)

	// Create.
	resp = postJSON(t, srv, "/api/v1/folders", map[string]any{"name": "Reactor"})
	assertStatus(t, resp, http.StatusCreated)
	var created struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &created)
	if created.ID == "" {
		t.Fatal("expected folder id")
	}

	// Update.
	resp = putRaw(t, srv, "/api/v1/folders/"+created.ID, "application/json",
		[]byte(`{"name":"Reactor Hall"}`))
	assertStatus(t, resp, http.StatusNoContent)

	// Update with empty name → 400.
	resp = putRaw(t, srv, "/api/v1/folders/"+created.ID, "application/json", []byte(`{"name":""}`))
	assertStatus(t, resp, http.StatusBadRequest)

	// Update unknown folder → 404.
	resp = putRaw(t, srv, "/api/v1/folders/fld-nope", "application/json", []byte(`{"name":"x"}`))
	assertStatus(t, resp, http.StatusNotFound)

	// It now appears in the list.
	resp = get(t, srv, "/api/v1/folders")
	assertStatus(t, resp, http.StatusOK)
	readJSON(t, resp, &list)
	if len(list) != 1 {
		t.Fatalf("expected 1 folder, got %d", len(list))
	}

	// Delete.
	resp = deleteReq(t, srv, "/api/v1/folders/"+created.ID)
	assertStatus(t, resp, http.StatusNoContent)

	// Delete unknown → 404.
	resp = deleteReq(t, srv, "/api/v1/folders/fld-nope")
	assertStatus(t, resp, http.StatusNotFound)
}

// ── /interfaces/{id}/acl ──────────────────────────────────────────────────────

func TestPanelACL(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
	assertStatus(t, resp, http.StatusCreated)
	var created struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &created)

	// GET ACL of a fresh panel.
	resp = get(t, srv, "/api/v1/interfaces/"+created.ID+"/acl")
	assertStatus(t, resp, http.StatusOK)
	var acl map[string]any
	readJSON(t, resp, &acl)
	if _, ok := acl["perm"]; !ok {
		t.Error("ACL response missing perm")
	}

	// PUT ACL — set a public read level.
	resp = putRaw(t, srv, "/api/v1/interfaces/"+created.ID+"/acl", "application/json",
		[]byte(`{"public":"read","grants":[]}`))
	assertStatus(t, resp, http.StatusNoContent)

	// PUT ACL on a non-existent panel → 404.
	resp = putRaw(t, srv, "/api/v1/interfaces/nope/acl", "application/json",
		[]byte(`{"public":"read"}`))
	assertStatus(t, resp, http.StatusNotFound)

	// PUT ACL referencing an unknown folder → 400.
	resp = putRaw(t, srv, "/api/v1/interfaces/"+created.ID+"/acl", "application/json",
		[]byte(`{"folder":"fld-nope"}`))
	assertStatus(t, resp, http.StatusBadRequest)
}

// ── /interfaces/reorder ───────────────────────────────────────────────────────

func TestReorderInterfaces(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	mk := func() string {
		resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
		assertStatus(t, resp, http.StatusCreated)
		var c struct {
			ID string `json:"id"`
		}
		readJSON(t, resp, &c)
		return c.ID
	}
	a, b := mk(), mk()

	// Reorder at root (no folder).
	resp := postJSON(t, srv, "/api/v1/interfaces/reorder", map[string]any{"ids": []string{b, a}})
	assertStatus(t, resp, http.StatusNoContent)

	// Reorder into a non-existent folder → 404.
	resp = postJSON(t, srv, "/api/v1/interfaces/reorder",
		map[string]any{"folder": "fld-nope", "ids": []string{a}})
	assertStatus(t, resp, http.StatusNotFound)

	// Malformed body → 400.
	resp = postRaw(t, srv, "/api/v1/interfaces/reorder", "application/json", []byte(`{not json`))
	assertStatus(t, resp, http.StatusBadRequest)
}

// ── /interfaces/{id}/versions ─────────────────────────────────────────────────

func TestInterfaceVersioning(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := postRaw(t, srv, "/api/v1/interfaces", "application/xml", []byte(sampleXML))
	assertStatus(t, resp, http.StatusCreated)
	var created struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &created)
	id := created.ID

	// Update once to create a backup (v1) and bump current to v2.
	upd := `<interface id="" name="Test Panel" version="2" w="800" h="600"></interface>`
	resp = putRaw(t, srv, "/api/v1/interfaces/"+id, "application/xml", []byte(upd))
	assertStatus(t, resp, http.StatusNoContent)

	// List versions — at least the backup.
	resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions")
	assertStatus(t, resp, http.StatusOK)
	var versions []map[string]any
	readJSON(t, resp, &versions)
	if len(versions) < 2 {
		t.Fatalf("expected >=2 versions, got %d", len(versions))
	}

	// Get the v1 backup.
	resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions/1")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Bad version path → 400.
	resp = get(t, srv, "/api/v1/interfaces/"+id+"/versions/abc")
	assertStatus(t, resp, http.StatusBadRequest)

	// Tag v1.
	resp = putRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/tag?tag=golden", "application/json", nil)
	assertStatus(t, resp, http.StatusNoContent)

	// Promote v1 → becomes new current.
	resp = postRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/promote", "application/json", nil)
	assertStatus(t, resp, http.StatusNoContent)

	// Fork v1 → brand-new panel.
	resp = postRaw(t, srv, "/api/v1/interfaces/"+id+"/versions/1/fork", "application/json", nil)
	assertStatus(t, resp, http.StatusCreated)
	var fork struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &fork)
	if fork.ID == "" || fork.ID == id {
		t.Errorf("fork id = %q, want fresh id", fork.ID)
	}

	// Version ops on a missing panel → 404.
	resp = get(t, srv, "/api/v1/interfaces/missing/versions")
	assertStatus(t, resp, http.StatusNotFound)
}

// ── /controllogic ─────────────────────────────────────────────────────────────

func TestControlLogicCRUD(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	// Empty list initially.
	resp := get(t, srv, "/api/v1/controllogic")
	assertStatus(t, resp, http.StatusOK)
	var list []any
	readJSON(t, resp, &list)
	if len(list) != 0 {
		t.Fatalf("expected no control logic, got %d", len(list))
	}

	// Create.
	resp = postJSON(t, srv, "/api/v1/controllogic", map[string]any{"name": "Watchdog", "enabled": true})
	assertStatus(t, resp, http.StatusCreated)
	var g struct {
		ID      string `json:"id"`
		Version int    `json:"version"`
	}
	readJSON(t, resp, &g)
	if g.ID == "" {
		t.Fatal("expected control logic id")
	}

	// Get.
	resp = get(t, srv, "/api/v1/controllogic/"+g.ID)
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Get missing → 404.
	resp = get(t, srv, "/api/v1/controllogic/nope")
	assertStatus(t, resp, http.StatusNotFound)

	// Update (bumps version, creates a backup).
	resp = putRaw(t, srv, "/api/v1/controllogic/"+g.ID, "application/json",
		[]byte(`{"name":"Watchdog v2","enabled":false}`))
	assertStatus(t, resp, http.StatusOK)

	// List versions.
	resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions")
	assertStatus(t, resp, http.StatusOK)
	var versions []map[string]any
	readJSON(t, resp, &versions)
	if len(versions) < 2 {
		t.Fatalf("expected >=2 control-logic versions, got %d", len(versions))
	}

	// Promote v1.
	resp = postRaw(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1/promote", "application/json", nil)
	assertStatus(t, resp, http.StatusOK)

	// Fork v1 → new graph id.
	resp = postRaw(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1/fork", "application/json", nil)
	assertStatus(t, resp, http.StatusCreated)
	var fork struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &fork)
	if fork.ID == "" || fork.ID == g.ID {
		t.Errorf("fork id = %q, want fresh id", fork.ID)
	}

	// Delete.
	resp = deleteReq(t, srv, "/api/v1/controllogic/"+g.ID)
	assertStatus(t, resp, http.StatusNoContent)

	// Delete missing → 404.
	resp = deleteReq(t, srv, "/api/v1/controllogic/"+g.ID)
	assertStatus(t, resp, http.StatusNotFound)
}

// ── /audit (Nop audit log, anonymous can view on an open policy) ───────────────

func TestGetAuditOpen(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := get(t, srv, "/api/v1/audit")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Bad start time → 400.
	resp = get(t, srv, "/api/v1/audit?start=not-a-time")
	assertStatus(t, resp, http.StatusBadRequest)
}

// ── /synthetic disabled guards ────────────────────────────────────────────────

// Synthetic is nil in the default harness, so every route must report 503
// (service unavailable) rather than panicking.
func TestSyntheticDisabledRoutes(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	cases := []struct {
		method, path string
		body         []byte
	}{
		{http.MethodGet, "/api/v1/synthetic/x", nil},
		{http.MethodPut, "/api/v1/synthetic/x", []byte(`{}`)},
		{http.MethodDelete, "/api/v1/synthetic/x", nil},
		{http.MethodPost, "/api/v1/synthetic/trace", []byte(`{}`)},
		{http.MethodGet, "/api/v1/synthetic/x/versions", nil},
		{http.MethodGet, "/api/v1/synthetic/x/versions/1", nil},
		{http.MethodPost, "/api/v1/synthetic/x/versions/1/promote", nil},
		{http.MethodPost, "/api/v1/synthetic/x/versions/1/fork", nil},
	}
	for _, c := range cases {
		var resp *http.Response
		switch c.method {
		case http.MethodGet:
			resp = get(t, srv, c.path)
		case http.MethodPut:
			resp = putRaw(t, srv, c.path, "application/json", c.body)
		case http.MethodDelete:
			resp = deleteReq(t, srv, c.path)
		case http.MethodPost:
			resp = postRaw(t, srv, c.path, "application/json", c.body)
		}
		if resp.StatusCode != http.StatusServiceUnavailable {
			t.Errorf("%s %s: status %d, want 503", c.method, c.path, resp.StatusCode)
		}
		resp.Body.Close()
	}
}

// ── /controllogic/{id}/versions/{version} ─────────────────────────────────────

func TestControlLogicGetVersion(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	resp := postJSON(t, srv, "/api/v1/controllogic", map[string]any{"name": "G"})
	assertStatus(t, resp, http.StatusCreated)
	var g struct {
		ID string `json:"id"`
	}
	readJSON(t, resp, &g)

	// Bump so a v1 backup exists.
	resp = putRaw(t, srv, "/api/v1/controllogic/"+g.ID, "application/json", []byte(`{"name":"G2"}`))
	assertStatus(t, resp, http.StatusOK)

	resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/1")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Bad version number → 400.
	resp = get(t, srv, "/api/v1/controllogic/"+g.ID+"/versions/xyz")
	assertStatus(t, resp, http.StatusBadRequest)
}

// ── /config/instances/diff ────────────────────────────────────────────────────

func TestDiffConfigInstancesMissing(t *testing.T) {
	srv, teardown := setup(t)
	defer teardown()

	// Missing left id → 400.
	resp := get(t, srv, "/api/v1/config/instances/diff?a=&b=")
	assertStatus(t, resp, http.StatusBadRequest)
}
