package api_test

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/uopi/uopi/internal/access"
	"github.com/uopi/uopi/internal/api"
	"github.com/uopi/uopi/internal/audit"
	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/confmgr"
	"github.com/uopi/uopi/internal/controllogic"
	"github.com/uopi/uopi/internal/datasource/stub"
	"github.com/uopi/uopi/internal/datasource/synthetic"
	"github.com/uopi/uopi/internal/panelacl"
	"github.com/uopi/uopi/internal/storage"
)

// setupSynthetic builds an API server whose synthetic data source is enabled, so
// the synthetic CRUD/versioning/trace handlers run their real bodies.
func setupSynthetic(t *testing.T) (*httptest.Server, func()) {
	t.Helper()
	ctx, cancel := context.WithCancel(context.Background())
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	brk := broker.New(ctx, log)
	ds := stub.New()
	if err := ds.Connect(ctx); err != nil {
		t.Fatal("stub connect:", err)
	}
	brk.Register(ds)

	dir := t.TempDir()
	store, _ := storage.New(dir)
	acl, _ := panelacl.New(dir)
	clStore, _ := controllogic.NewStore(dir)
	cfgStore, _ := confmgr.New(dir)
	clEngine := controllogic.NewEngine(ctx, brk, clStore, cfgStore, audit.Nop(), log)

	synthDS := synthetic.New(dir, brk, log)
	if err := synthDS.Connect(ctx); err != nil {
		t.Fatal("synthetic connect:", err)
	}
	brk.Register(synthDS)

	mux := http.NewServeMux()
	api.New(brk, synthDS, store, cfgStore, access.New("", nil), acl, clStore, clEngine, audit.Nop(), "", "", 0, log).Register(mux, "/api/v1")

	srv := httptest.NewServer(mux)
	return srv, func() { srv.Close(); cancel() }
}

// putJSON marshals body and issues a PUT, mirroring postJSON.
func putJSON(t *testing.T, srv *httptest.Server, path string, body any) *http.Response {
	t.Helper()
	b, err := json.Marshal(body)
	if err != nil {
		t.Fatal("json.Marshal:", err)
	}
	return putRaw(t, srv, path, "application/json", b)
}

// syntheticBody returns a minimal valid graph signal sourced from the stub's
// "sine" signal through a gain op.
func syntheticBody(name string) map[string]any {
	return map[string]any{
		"name":       name,
		"visibility": "global",
		"graph": map[string]any{
			"output": "out",
			"nodes": []map[string]any{
				{"id": "a", "kind": "source", "ds": "stub", "signal": "sine"},
				{"id": "g", "kind": "op", "op": "gain", "inputs": []string{"a"},
					"params": map[string]any{"k": 2.0}},
				{"id": "out", "kind": "output", "inputs": []string{"g"}},
			},
		},
	}
}

func TestSyntheticCRUDEnabled(t *testing.T) {
	srv, teardown := setupSynthetic(t)
	defer teardown()

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

	// Create.
	resp = postJSON(t, srv, "/api/v1/synthetic", syntheticBody("doubled"))
	assertStatus(t, resp, http.StatusCreated)
	resp.Body.Close()

	// Duplicate create → 400 (AddSignal rejects an existing name).
	resp = postJSON(t, srv, "/api/v1/synthetic", syntheticBody("doubled"))
	assertStatus(t, resp, http.StatusBadRequest)

	// Invalid JSON → 400.
	resp = postRaw(t, srv, "/api/v1/synthetic", "application/json", []byte(`{bad`))
	assertStatus(t, resp, http.StatusBadRequest)

	// Get.
	resp = get(t, srv, "/api/v1/synthetic/doubled")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

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

	// Update (changes gain factor).
	upd := syntheticBody("doubled")
	upd["graph"].(map[string]any)["nodes"].([]map[string]any)[1]["params"] = map[string]any{"k": 3.0}
	resp = putJSON(t, srv, "/api/v1/synthetic/doubled", upd)
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Update missing → 404.
	resp = putJSON(t, srv, "/api/v1/synthetic/nope", syntheticBody("nope"))
	assertStatus(t, resp, http.StatusNotFound)

	// Versions list (the update created a backup).
	resp = get(t, srv, "/api/v1/synthetic/doubled/versions")
	assertStatus(t, resp, http.StatusOK)
	var versions []map[string]any
	readJSON(t, resp, &versions)
	if len(versions) < 2 {
		t.Fatalf("expected >=2 synthetic versions, got %d", len(versions))
	}

	// Get v1.
	resp = get(t, srv, "/api/v1/synthetic/doubled/versions/1")
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Bad version → 400.
	resp = get(t, srv, "/api/v1/synthetic/doubled/versions/xyz")
	assertStatus(t, resp, http.StatusBadRequest)

	// Promote v1.
	resp = postRaw(t, srv, "/api/v1/synthetic/doubled/versions/1/promote", "application/json", nil)
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Fork v1 → new signal.
	resp = postRaw(t, srv, "/api/v1/synthetic/doubled/versions/1/fork", "application/json", nil)
	assertStatus(t, resp, http.StatusCreated)
	resp.Body.Close()

	// Trace an unsaved graph.
	resp = postJSON(t, srv, "/api/v1/synthetic/trace", syntheticBody("scratch"))
	assertStatus(t, resp, http.StatusOK)
	resp.Body.Close()

	// Trace with invalid JSON → 400.
	resp = postRaw(t, srv, "/api/v1/synthetic/trace", "application/json", []byte(`{bad`))
	assertStatus(t, resp, http.StatusBadRequest)

	// Delete.
	resp = deleteReq(t, srv, "/api/v1/synthetic/doubled")
	assertStatus(t, resp, http.StatusNoContent)

	// Delete missing → 404.
	resp = deleteReq(t, srv, "/api/v1/synthetic/doubled")
	assertStatus(t, resp, http.StatusNotFound)
}
