package api

import (
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/alexei/tinyforge/internal/auth"
	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/volsnap"
	"github.com/alexei/tinyforge/internal/webhook"
)

// newSnapshotEnv builds an API test env with the volume-snapshot engine wired
// (the shared newAPITestEnv does not wire it). dataDir holds the snapshot
// archives; baseVol is where host-bind volume directories resolve.
func newSnapshotEnv(t *testing.T) (*apiTestEnv, string) {
	t.Helper()
	st, err := store.New(":memory:")
	if err != nil {
		t.Fatalf("create store: %v", err)
	}
	t.Cleanup(func() { st.Close() })

	encKey := [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	dispatcher := &fakeAPIDispatcher{}
	wh := webhook.NewHandler(st)
	wh.SetPluginDispatcher(dispatcher)
	srv := NewServer(st, nil, nil, nil, dispatcher, nil, wh, nil, encKey)

	snapEng, err := volsnap.New(st, t.TempDir())
	if err != nil {
		t.Fatalf("snapshot engine: %v", err)
	}
	srv.SetSnapshotEngine(snapEng)

	httpsrv := httptest.NewServer(srv.Router())
	t.Cleanup(httpsrv.Close)

	la := auth.NewLocalAuth(encKey)
	tok, err := la.GenerateToken(auth.Claims{UserID: "u-admin", Username: "admin", Role: "admin"})
	if err != nil {
		t.Fatalf("mint token: %v", err)
	}

	baseVol := t.TempDir()
	settings, _ := st.GetSettings()
	settings.BaseVolumePath = baseVol
	if err := st.UpdateSettings(settings); err != nil {
		t.Fatalf("update settings: %v", err)
	}

	return &apiTestEnv{srv: httpsrv, store: st, dispatcher: dispatcher, adminToken: tok.Token, encKey: encKey}, baseVol
}

func TestVolumeSnapshots_EndToEnd(t *testing.T) {
	e, baseVol := newSnapshotEnv(t)

	w, err := e.store.CreateWorkload(store.Workload{
		Name:         "data-app",
		Kind:         "project",
		SourceKind:   "image",
		SourceConfig: `{"image":"registry.example.com/owner/app","port":8080}`,
	})
	if err != nil {
		t.Fatalf("create workload: %v", err)
	}
	if _, err := e.store.SetWorkloadVolume(store.WorkloadVolume{
		WorkloadID: w.ID, Target: "/data", Source: "data", Scope: "project",
	}); err != nil {
		t.Fatalf("set volume: %v", err)
	}

	// Materialize the resolved host-bind dir with a file so there is data to
	// capture. Layout mirrors ResolveWorkloadPath for project scope:
	// <baseVol>/<name>-<id8>/<source>.
	id8 := w.ID
	if len(id8) > 8 {
		id8 = id8[:8]
	}
	hostDir := filepath.Join(baseVol, "data-app-"+id8, "data")
	if err := os.MkdirAll(hostDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(hostDir, "payload.txt"), []byte("important"), 0o644); err != nil {
		t.Fatal(err)
	}

	// snapshotable lists the one host-bind volume.
	resp := e.do(t, http.MethodGet, "/api/workloads/"+w.ID+"/snapshotable", nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("snapshotable status = %d", resp.StatusCode)
	}
	var snapable struct {
		Volumes []map[string]string `json:"volumes"`
		Skipped []map[string]string `json:"skipped"`
	}
	decodeEnvelope(t, resp, &snapable)
	if len(snapable.Volumes) != 1 || snapable.Volumes[0]["target"] != "/data" {
		t.Fatalf("expected 1 snapshotable volume /data, got %+v", snapable)
	}

	// Create a snapshot.
	resp = e.do(t, http.MethodPost, "/api/workloads/"+w.ID+"/snapshots", map[string]string{"label": "before upgrade"})
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("create snapshot status = %d", resp.StatusCode)
	}
	var snap store.VolumeSnapshot
	decodeEnvelope(t, resp, &snap)
	if snap.ID == "" || snap.SizeBytes == 0 || snap.Label != "before upgrade" {
		t.Fatalf("unexpected snapshot: %+v", snap)
	}

	// It appears in the list.
	resp = e.do(t, http.MethodGet, "/api/workloads/"+w.ID+"/snapshots", nil)
	var list []store.VolumeSnapshot
	decodeEnvelope(t, resp, &list)
	if len(list) != 1 || list[0].ID != snap.ID {
		t.Fatalf("expected 1 snapshot in list, got %+v", list)
	}

	// Download streams a non-empty gzip archive (not the JSON envelope).
	resp = e.do(t, http.MethodGet, "/api/snapshots/"+snap.ID+"/download", nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("download status = %d", resp.StatusCode)
	}
	if ct := resp.Header.Get("Content-Type"); ct != "application/gzip" {
		t.Errorf("download content-type = %q, want application/gzip", ct)
	}
	data, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	if len(data) == 0 {
		t.Error("download body is empty")
	}

	// Delete removes it.
	resp = e.do(t, http.MethodDelete, "/api/snapshots/"+snap.ID, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("delete status = %d", resp.StatusCode)
	}
	resp = e.do(t, http.MethodGet, "/api/workloads/"+w.ID+"/snapshots", nil)
	var after []store.VolumeSnapshot
	decodeEnvelope(t, resp, &after)
	if len(after) != 0 {
		t.Fatalf("expected 0 snapshots after delete, got %d", len(after))
	}
}

func TestCreateSnapshot_NoVolumeData_Returns400(t *testing.T) {
	e, _ := newSnapshotEnv(t)
	w, err := e.store.CreateWorkload(store.Workload{
		Name:         "no-vol-app",
		Kind:         "project",
		SourceKind:   "image",
		SourceConfig: `{"image":"x","port":80}`,
	})
	if err != nil {
		t.Fatalf("create workload: %v", err)
	}
	resp := e.do(t, http.MethodPost, "/api/workloads/"+w.ID+"/snapshots", nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400 for an app with no snapshottable volumes, got %d", resp.StatusCode)
	}
	resp.Body.Close()
}

func TestSnapshotEndpoints_RequireWorkload(t *testing.T) {
	e, _ := newSnapshotEnv(t)
	// snapshotable on an unknown workload → 404.
	resp := e.do(t, http.MethodGet, "/api/workloads/does-not-exist/snapshotable", nil)
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("snapshotable unknown workload = %d, want 404", resp.StatusCode)
	}
	resp.Body.Close()
}
