package downloads

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

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/storage"
)

// testCatalog is a deployment-style artifact catalog used by the handler tests.
// L0 no longer hardcodes product artifacts; the catalog is injected.
var testCatalog = Catalog{
	"cli":    {Key: "release-artifact/nexuscrm-connect/latest/cli.tgz", Filename: "nexuscrm-connect-cli.tgz"},
	"plugin": {Key: "release-artifact/nexuscrm-connect/latest/plugin.dxt", Filename: "nexuscrm-connect-plugin.dxt"},
}

func TestLoadCatalogFromEnv(t *testing.T) {
	// Unset → empty catalog (L0 app-blind by default).
	t.Setenv(DownloadArtifactsEnv, "")
	if cat, err := LoadCatalogFromEnv(); err != nil || len(cat) != 0 {
		t.Fatalf("empty env: got (%v, %v), want (empty, nil)", cat, err)
	}

	// Valid JSON → parsed catalog.
	t.Setenv(DownloadArtifactsEnv, `{"cli":{"key":"release-artifact/x/cli.tgz","filename":"x-cli.tgz"}}`)
	cat, err := LoadCatalogFromEnv()
	if err != nil {
		t.Fatalf("valid env: %v", err)
	}
	if a, ok := cat["cli"]; !ok || a.Key != "release-artifact/x/cli.tgz" || a.Filename != "x-cli.tgz" {
		t.Fatalf("valid env: parsed %#v", cat)
	}

	// Malformed JSON → error (deployment misconfig must surface).
	t.Setenv(DownloadArtifactsEnv, `{not json`)
	if _, err := LoadCatalogFromEnv(); err == nil {
		t.Fatal("malformed env: expected an error")
	}
}

// fakeStore is a minimal ObjectStore stand-in for handler tests. It optionally
// serves a presigned URL or an in-memory object, and returns not-found for
// anything else.
type fakeStore struct {
	presignURL  string            // if set, GeneratePresignedGetURL returns it
	presignErr  error             // otherwise GeneratePresignedGetURL returns this
	objects     map[string][]byte // available objects for streaming
	headMissing bool              // force HeadObject to report the object absent
}

func (f *fakeStore) PutObject(context.Context, string, io.Reader, storage.PutObjectOptions) (storage.ObjectMetadata, error) {
	return storage.ObjectMetadata{}, nil
}

func (f *fakeStore) GetObject(_ context.Context, key string) (io.ReadCloser, storage.ObjectMetadata, error) {
	data, ok := f.objects[key]
	if !ok {
		return nil, storage.ObjectMetadata{}, storage.ErrObjectNotFound
	}
	return io.NopCloser(strings.NewReader(string(data))), storage.ObjectMetadata{
		Key:  key,
		Size: int64(len(data)),
	}, nil
}

func (f *fakeStore) HeadObject(_ context.Context, key string) (storage.ObjectMetadata, error) {
	if f.headMissing {
		return storage.ObjectMetadata{}, storage.ErrObjectNotFound
	}
	if data, ok := f.objects[key]; ok {
		return storage.ObjectMetadata{Key: key, Size: int64(len(data))}, nil
	}
	// A presign-capable provider (S3) has the object even though this in-memory
	// fake doesn't stream it; report it present so presign can proceed.
	if f.presignURL != "" {
		return storage.ObjectMetadata{Key: key}, nil
	}
	return storage.ObjectMetadata{}, storage.ErrObjectNotFound
}

func (f *fakeStore) DeleteObject(context.Context, string) error { return nil }

func (f *fakeStore) GeneratePresignedGetURL(context.Context, string, time.Duration) (string, error) {
	if f.presignURL != "" {
		return f.presignURL, nil
	}
	return "", f.presignErr
}

func newRouter(store storage.ObjectStore) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	RegisterDownloadRoutes(r, NewHandler(store, testCatalog))
	return r
}

func TestLatestUnknownKindReturns404(t *testing.T) {
	r := newRouter(&fakeStore{presignErr: storage.ErrPresignUnsupported})
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/bogus/latest", nil))
	if w.Code != http.StatusNotFound {
		t.Fatalf("unknown kind: got status %d, want 404", w.Code)
	}
}

func TestLatestPresignRedirects(t *testing.T) {
	r := newRouter(&fakeStore{presignURL: "https://example.com/signed"})
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/cli/latest", nil))
	if w.Code != http.StatusFound {
		t.Fatalf("presign: got status %d, want 302", w.Code)
	}
	if loc := w.Header().Get("Location"); loc != "https://example.com/signed" {
		t.Fatalf("presign: Location = %q, want signed URL", loc)
	}
}

func TestLatestStreamsWhenPresignUnsupported(t *testing.T) {
	key, filename := testCatalog["plugin"].Key, testCatalog["plugin"].Filename
	r := newRouter(&fakeStore{
		presignErr: storage.ErrPresignUnsupported,
		objects:    map[string][]byte{key: []byte("PAYLOAD")},
	})
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/plugin/latest", nil))
	if w.Code != http.StatusOK {
		t.Fatalf("stream: got status %d, want 200", w.Code)
	}
	if ct := w.Header().Get("Content-Type"); ct != "application/octet-stream" {
		t.Fatalf("stream: Content-Type = %q", ct)
	}
	if cd := w.Header().Get("Content-Disposition"); !strings.Contains(cd, filename) {
		t.Fatalf("stream: Content-Disposition = %q, want filename %q", cd, filename)
	}
	if w.Body.String() != "PAYLOAD" {
		t.Fatalf("stream: body = %q", w.Body.String())
	}
}

func TestLatestUsesDedicatedArtifactFilesystemWhenConfigured(t *testing.T) {
	key := testCatalog["plugin"].Key
	runtimeRoot := t.TempDir()
	t.Setenv("NEXUSCRM_RUNTIME_ROOT", runtimeRoot)
	t.Setenv(DownloadArtifactStorageRootEnv, "release-artifacts")
	primary := &fakeStore{
		presignErr: storage.ErrPresignUnsupported,
		objects:    map[string][]byte{key: []byte("PRIMARY-STORE")},
	}
	artifactStore, err := ArtifactStoreFromEnv(primary)
	if err != nil {
		t.Fatalf("artifact store: %v", err)
	}
	if _, err := artifactStore.PutObject(
		context.Background(),
		key,
		strings.NewReader("DEDICATED-ARTIFACT"),
		storage.PutObjectOptions{},
	); err != nil {
		t.Fatalf("seed dedicated artifact: %v", err)
	}

	r := newRouter(artifactStore)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/plugin/latest", nil))
	if w.Code != http.StatusOK {
		t.Fatalf("dedicated store: got status %d, want 200", w.Code)
	}
	if w.Body.String() != "DEDICATED-ARTIFACT" {
		t.Fatalf("dedicated store: body = %q", w.Body.String())
	}
	artifactPath := filepath.Join(runtimeRoot, "release-artifacts", filepath.FromSlash(key))
	if data, err := os.ReadFile(artifactPath); err != nil || string(data) != "DEDICATED-ARTIFACT" {
		t.Fatalf("runtime-root artifact path = (%q, %v), want dedicated bytes", data, err)
	}
}

func TestNewArtifactStoreFromEnvFallsBackToCustomerStore(t *testing.T) {
	runtimeRoot := t.TempDir()
	t.Setenv("NEXUSCRM_RUNTIME_ROOT", runtimeRoot)
	t.Setenv(DownloadArtifactStorageRootEnv, "")
	t.Setenv("FILE_STORAGE_PROVIDER", storage.ProviderFilesystem)
	t.Setenv("FILE_STORAGE_ROOT", "customer-files")

	store, err := NewArtifactStoreFromEnv()
	if err != nil {
		t.Fatalf("fallback artifact store: %v", err)
	}
	const key = "release-artifact/fallback/plugin.dxt"
	if _, err := store.PutObject(
		context.Background(),
		key,
		strings.NewReader("LEGACY-SHARED-STORE"),
		storage.PutObjectOptions{},
	); err != nil {
		t.Fatalf("write fallback artifact: %v", err)
	}

	path := filepath.Join(runtimeRoot, "customer-files", filepath.FromSlash(key))
	if data, err := os.ReadFile(path); err != nil || string(data) != "LEGACY-SHARED-STORE" {
		t.Fatalf("fallback artifact path = (%q, %v), want legacy shared bytes", data, err)
	}
}

func TestLatestStreamMissingObjectReturns404(t *testing.T) {
	r := newRouter(&fakeStore{presignErr: storage.ErrPresignUnsupported, objects: map[string][]byte{}})
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/cli/latest", nil))
	if w.Code != http.StatusNotFound {
		t.Fatalf("missing object: got status %d, want 404", w.Code)
	}
}

// A presign-capable provider (S3) signs URLs without checking existence, so a
// missing object must be caught by the HEAD probe and 404 — never a 302 to a
// dead link.
func TestLatestPresignMissingObjectReturns404(t *testing.T) {
	r := newRouter(&fakeStore{presignURL: "https://example.com/signed", headMissing: true})
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/downloads/cli/latest", nil))
	if w.Code != http.StatusNotFound {
		t.Fatalf("missing presign object: got status %d, want 404", w.Code)
	}
	if loc := w.Header().Get("Location"); loc != "" {
		t.Fatalf("missing presign object must not redirect, got Location %q", loc)
	}
}
