package http

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	stdhttp "net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/crypto"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/server/config"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
)

// newTestServer wires up a Server with file-backed sqlite + a fresh
// AEAD key + an httptest harness. Returns the Server, its base URL,
// and the bootstrap token (caller decides whether to consume it).
func newTestServer(t *testing.T, withBootstrapToken bool) (*Server, string) {
	t.Helper()

	dir := t.TempDir()
	st, err := store.Open(context.Background(), filepath.Join(dir, "rm.db"))
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	keyPath := filepath.Join(dir, "secret.key")
	if err := crypto.GenerateKeyFile(keyPath); err != nil {
		t.Fatalf("genkey: %v", err)
	}
	key, _ := crypto.LoadKeyFromFile(keyPath)
	aead, _ := crypto.NewAEAD(key)

	deps := Deps{
		Cfg:   config.Config{Listen: ":0", DataDir: dir, SecretKeyFile: keyPath},
		Store: st,
		AEAD:  aead,
	}
	if withBootstrapToken {
		deps.BootstrapToken = "test-token"
	}

	s := New(deps)
	ts := httptest.NewServer(s.srv.Handler)
	t.Cleanup(ts.Close)
	return s, ts.URL
}

func TestHealthz(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, false)

	res, err := stdhttp.Get(url + "/healthz")
	if err != nil {
		t.Fatalf("GET: %v", err)
	}
	defer res.Body.Close()
	if res.StatusCode != stdhttp.StatusNoContent {
		t.Errorf("status: %d", res.StatusCode)
	}
}

func TestBootstrapHappyPath(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, true)

	body, _ := json.Marshal(bootstrapRequest{
		Token: "test-token", Username: "alice", Password: "averylongpassword",
	})
	res, err := stdhttp.Post(url+"/api/bootstrap", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer res.Body.Close()
	if res.StatusCode != stdhttp.StatusCreated {
		got, _ := io.ReadAll(res.Body)
		t.Fatalf("status %d: %s", res.StatusCode, got)
	}

	// Re-running bootstrap must fail now that a user exists.
	res2, _ := stdhttp.Post(url+"/api/bootstrap", "application/json", bytes.NewReader(body))
	defer res2.Body.Close()
	if res2.StatusCode != stdhttp.StatusConflict {
		t.Errorf("second bootstrap: want 409, got %d", res2.StatusCode)
	}
}

func TestBootstrapBadToken(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, true)

	body, _ := json.Marshal(bootstrapRequest{
		Token: "wrong", Username: "alice", Password: "averylongpassword",
	})
	res, _ := stdhttp.Post(url+"/api/bootstrap", "application/json", bytes.NewReader(body))
	defer res.Body.Close()
	if res.StatusCode != stdhttp.StatusUnauthorized {
		t.Errorf("status: %d", res.StatusCode)
	}
}

func TestBootstrapWeakPassword(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, true)

	body, _ := json.Marshal(bootstrapRequest{
		Token: "test-token", Username: "alice", Password: "short",
	})
	res, _ := stdhttp.Post(url+"/api/bootstrap", "application/json", bytes.NewReader(body))
	defer res.Body.Close()
	if res.StatusCode != stdhttp.StatusBadRequest {
		t.Errorf("status: %d", res.StatusCode)
	}
}

func TestLoginAndLogout(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, true)

	// Bootstrap the admin first.
	bs, _ := json.Marshal(bootstrapRequest{
		Token: "test-token", Username: "alice", Password: "averylongpassword",
	})
	if bsRes, err := stdhttp.Post(url+"/api/bootstrap", "application/json", bytes.NewReader(bs)); err == nil {
		_ = bsRes.Body.Close()
	}

	// Login.
	body, _ := json.Marshal(loginRequest{Username: "alice", Password: "averylongpassword"})
	res, err := stdhttp.Post(url+"/api/auth/login", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("login: %v", err)
	}
	defer res.Body.Close()
	if res.StatusCode != stdhttp.StatusOK {
		got, _ := io.ReadAll(res.Body)
		t.Fatalf("login status %d: %s", res.StatusCode, got)
	}
	cookies := res.Cookies()
	var sess *stdhttp.Cookie
	for _, c := range cookies {
		if c.Name == sessionCookieName {
			sess = c
			break
		}
	}
	if sess == nil {
		t.Fatal("no session cookie set")
	}
	if !sess.HttpOnly {
		t.Error("session cookie should be HttpOnly")
	}

	// Bad password → 401.
	bad, _ := json.Marshal(loginRequest{Username: "alice", Password: "wrong"})
	res2, _ := stdhttp.Post(url+"/api/auth/login", "application/json", bytes.NewReader(bad))
	defer res2.Body.Close()
	if res2.StatusCode != stdhttp.StatusUnauthorized {
		t.Errorf("bad pass: want 401, got %d", res2.StatusCode)
	}

	// Logout deletes the cookie + the server-side session.
	logoutReq, _ := stdhttp.NewRequest(stdhttp.MethodPost, url+"/api/auth/logout", nil)
	logoutReq.AddCookie(sess)
	res3, err := stdhttp.DefaultClient.Do(logoutReq)
	if err != nil {
		t.Fatalf("logout: %v", err)
	}
	defer res3.Body.Close()
	if res3.StatusCode != stdhttp.StatusNoContent {
		t.Errorf("logout status: %d", res3.StatusCode)
	}
}

func TestLoginUnknownUserSameAsBadPassword(t *testing.T) {
	t.Parallel()
	_, url := newTestServer(t, false)

	body, _ := json.Marshal(loginRequest{Username: "ghost", Password: "anything12345"})
	res, _ := stdhttp.Post(url+"/api/auth/login", "application/json", bytes.NewReader(body))
	defer res.Body.Close()
	// Both unknown-user and bad-password must return 401 with the same
	// code so a probe can't enumerate usernames.
	if res.StatusCode != stdhttp.StatusUnauthorized {
		t.Errorf("unknown user: want 401, got %d", res.StatusCode)
	}
}
