package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/http/httptest"
	"os"
	"strconv"
	"testing"
	"time"

	"party.at/party/cmd/party/common"
	"party.at/party/internal/data"
	"party.at/party/internal/jsonlog"
)

func newTestApplication(t *testing.T) *common.Application {
	var cfg common.Config
	cfg.DB.DSN          = "postgres://party:password@localhost:5432/party?sslmode=disable"
	cfg.DB.MaxOpenConns = 25
	cfg.DB.MaxIdleConns = 25
	cfg.DB.MaxIdleTime  = "15m"
	cfg.Limiter.Enabled = false
	cfg.Env             = "development"

	logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo)

	db, err := openDB(cfg)
	if err != nil {
		logger.PrintFatal(err, nil)
	}
	t.Cleanup(func() { db.Close() })

	app := &common.Application{
		Logger: logger,
		Models: data.NewModels(db),
		Config: cfg,
	}

	return app
}

type testServer struct {
	*httptest.Server
	app *common.Application
}

func newTestServer(t *testing.T, app *common.Application, h http.Handler) *testServer {
	ts := httptest.NewTLSServer(h)
	return &testServer{ts, app}
}

func (ts *testServer) postJSON(t *testing.T, path string, body any) (int, http.Header, []byte) {
	t.Helper()
	b, err := json.Marshal(body)
	if err != nil {
		t.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost, ts.URL+path, bytes.NewReader(b))
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := ts.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	return resp.StatusCode, resp.Header, respBody
}

func (ts *testServer) get(t *testing.T, path string) (int, http.Header, []byte) {
	t.Helper()
	rs, err := ts.Client().Get(ts.URL + path)
	if err != nil {
		t.Fatal(err)
	}
	defer rs.Body.Close()
	body, err := io.ReadAll(rs.Body)
	if err != nil {
		t.Fatal(err)
	}
	return rs.StatusCode, rs.Header, body
}

func (ts *testServer) registerAndLogin(t *testing.T, email, password string, roles ...string) string {
	t.Helper()

	var role string
	if len(role) > 0 {
		role = roles[0]
	}

	registerBody := map[string]any{
		"email":       email,
		"password":    password,
		"username":    email,
		"name":        "Test User",
		"alt_name":    "",
		"provider_id": 1,
		"role":        role,
	}
	code, _, body := ts.postJSON(t, "/v1/users", registerBody)
	if code != http.StatusCreated {
		t.Fatalf("register: want 201 got %d: %s", code, body)
	}
	loginBody := map[string]string{"email": email, "password": password}
	code, _, body = ts.postJSON(t, "/v1/tokens/authentication", loginBody)
	if code != http.StatusCreated {
		t.Fatalf("login: want 201 got %d: %s", code, body)
	}
	var resp struct {
		AuthenticationToken struct {
			Token string `json:"token"`
		} `json:"authentication_token"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		t.Fatalf("parse token: %v", err)
	}
	return resp.AuthenticationToken.Token
}

func (ts *testServer) getWithToken(t *testing.T, path, token string) (int, http.Header, []byte) {
	t.Helper()
	req, err := http.NewRequest(http.MethodGet, ts.URL+path, nil)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	rs, err := ts.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer rs.Body.Close()
	body, err := io.ReadAll(rs.Body)
	if err != nil {
		t.Fatal(err)
	}
	return rs.StatusCode, rs.Header, body
}

func (ts *testServer) postJSONWithToken(t *testing.T, path, token string, body any) (int, http.Header, []byte) {
	t.Helper()
	b, err := json.Marshal(body)
	if err != nil {
		t.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost, ts.URL+path, bytes.NewReader(b))
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	rs, err := ts.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer rs.Body.Close()
	respBody, err := io.ReadAll(rs.Body)
	if err != nil {
		t.Fatal(err)
	}
	return rs.StatusCode, rs.Header, respBody
}

func uniqueEmail() string {
	return fmt.Sprintf("test_%d@example.com", time.Now().UnixNano())
}

func (ts *testServer) assignRole(t *testing.T, email string, roleCode string) {
	t.Helper()
	user, err := ts.app.Models.Users.GetByEmail(email)
	if err != nil {
		t.Fatalf("assignRole: look up user %q: %v", email, err)
	}
	if err = ts.app.Models.Roles.AssignToUser(user.ID, roleCode); err != nil {
		t.Fatalf("assignRole: assign %q to user %d: %v", roleCode, user.ID, err)
	}
}

func (ts *testServer) setRole(t *testing.T, email, roleCode string) {
	t.Helper()
	user, err := ts.app.Models.Users.GetByEmail(email)
	if err != nil {
		t.Fatalf("setRole: look up user %q: %v", email, err)
	}
	if _, err = ts.app.Models.Roles.DB.Exec(`DELETE FROM users_roles WHERE user_id = $1`, user.ID); err != nil {
		t.Fatalf("setRole: clear roles for user %d: %v", user.ID, err)
	}
	if err = ts.app.Models.Roles.AssignToUser(user.ID, roleCode); err != nil {
		t.Fatalf("setRole: assign %q to user %d: %v", roleCode, user.ID, err)
	}
}

func generateBlindingFactor(n *big.Int) (*big.Int, error) {
	one := big.NewInt(1)
	for {
		r, err := randBigInt(n)
		if err != nil {
			return nil, err
		}
		if r.Cmp(one) <= 0 {
			continue
		}
		gcd := new(big.Int).GCD(nil, nil, r, n)
		if gcd.Cmp(one) == 0 {
			return r, nil
		}
	}
}

func randBigInt(max *big.Int) (*big.Int, error) {
	b := make([]byte, (max.BitLen()+7)/8)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}
	r := new(big.Int).SetBytes(b)
	return r.Mod(r, max), nil
}

func mustUnmarshal(t *testing.T, body []byte, v any) {
	t.Helper()
	if err := json.Unmarshal(body, v); err != nil {
		t.Fatalf("unmarshal: %v\nbody: %s", err, body)
	}
}

func i64str(n int64) string {
	return strconv.FormatInt(n, 10)
}
