package license

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestClientDetail(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/api/v1/licenses/detail" {
			t.Fatalf("request = %s %s", r.Method, r.URL.Path)
		}
		var request detailRequest
		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if request.LicenseCode != "AUTH-TEST" {
			t.Fatalf("licenseCode = %q", request.LicenseCode)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"valid":true,"type":"standard","version":"0.10"}`))
	}))
	defer server.Close()

	detail, err := NewClient(server.URL, time.Second).Detail(context.Background(), " AUTH-TEST ")
	if err != nil {
		t.Fatalf("Detail: %v", err)
	}
	if !detail.Valid || detail.Type != "standard" || detail.Version != "0.10" {
		t.Fatalf("detail = %#v", detail)
	}
}

func TestClientDetailRejectsInconsistentValidResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{"valid":true,"type":"","version":""}`))
	}))
	defer server.Close()

	if _, err := NewClient(server.URL, time.Second).Detail(context.Background(), "AUTH-TEST"); err == nil {
		t.Fatal("expected inconsistent response error")
	}
}

func TestClientDetailReturnsInvalidLicense(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{"valid":false,"type":"","version":""}`))
	}))
	defer server.Close()

	detail, err := NewClient(server.URL, time.Second).Detail(context.Background(), "AUTH-INVALID")
	if err != nil {
		t.Fatalf("Detail: %v", err)
	}
	if detail.Valid {
		t.Fatalf("detail = %#v", detail)
	}
}

func TestClientProbe(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet || r.URL.Path != "/api/health" {
			t.Fatalf("request = %s %s", r.Method, r.URL.Path)
		}
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	}))
	defer server.Close()

	if err := NewClient(server.URL, time.Second).Probe(context.Background()); err != nil {
		t.Fatalf("Probe: %v", err)
	}
}

func TestClientRegister(t *testing.T) {
	expiresAt := time.Date(2026, 7, 17, 12, 0, 0, 0, time.UTC)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/api/v1/instances/register" {
			t.Fatalf("request = %s %s", r.Method, r.URL.Path)
		}
		var request registerRequest
		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if request.LicenseCode != "AUTH-TEST" || request.InstanceID != "550e8400-e29b-41d4-a716-446655440000" {
			t.Fatalf("request = %#v", request)
		}
		if request.InstanceName != "production-01" || request.NodeRole != "master" || request.SystemName == "" || request.SystemArch == "" {
			t.Fatalf("request metadata = %#v", request)
		}
		_ = json.NewEncoder(w).Encode(registerResponse{
			Valid:         true,
			Type:          "standard",
			Version:       "0.10",
			InstanceToken: "inst_test",
			ExpiresAt:     expiresAt,
		})
	}))
	defer server.Close()

	result, err := NewClient(server.URL, time.Second).Register(context.Background(), RegisterRequest{
		LicenseCode:  " AUTH-TEST ",
		InstanceID:   " 550e8400-e29b-41d4-a716-446655440000 ",
		InstanceName: " production-01 ",
		NodeRole:     " master ",
		SystemName:   " linux ",
		SystemArch:   " amd64 ",
	})
	if err != nil {
		t.Fatalf("Register: %v", err)
	}
	if result.Type != "standard" || result.Version != "0.10" || result.InstanceToken != "inst_test" || !result.ExpiresAt.Equal(expiresAt) {
		t.Fatalf("registration = %#v", result)
	}
}

func TestClientRegisterSupportsLegacyMinimalResponse(t *testing.T) {
	expiresAt := time.Date(2026, 7, 17, 12, 0, 0, 0, time.UTC)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"instanceToken": "inst_test",
			"expiresAt":     expiresAt,
		})
	}))
	defer server.Close()

	result, err := NewClient(server.URL, time.Second).Register(context.Background(), RegisterRequest{})
	if err != nil {
		t.Fatalf("Register: %v", err)
	}
	if result.InstanceToken != "inst_test" || !result.ExpiresAt.Equal(expiresAt) {
		t.Fatalf("registration = %#v", result)
	}
}

func TestClientHeartbeat(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/licenses/public-key":
			_ = json.NewEncoder(w).Encode(publicKeyResponse{
				Algorithm: "Ed25519",
				PublicKey: base64.RawURLEncoding.EncodeToString(publicKey),
				KeyID:     "test-key",
			})
		case "/api/v1/instances/heartbeat":
			if r.Method != http.MethodPost {
				t.Fatalf("request = %s %s", r.Method, r.URL.Path)
			}
			if got := r.Header.Get("Authorization"); got != "Bearer inst_test" {
				t.Fatalf("authorization = %q", got)
			}
			var request heartbeatRequest
			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
				t.Fatalf("decode request: %v", err)
			}
			if request.InstanceID != "550e8400-e29b-41d4-a716-446655440000" || request.ReportedVersion != "0.1.0" || request.IP != "" || request.BackendURL != testBackendURL {
				t.Fatalf("request = %#v", request)
			}
			validUntil := time.Now().UTC().Add(time.Hour).Format(time.RFC3339)
			expiresAt := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339)
			payload := heartbeatSignedPayload{
				Status:     "active",
				ValidUntil: validUntil,
				ExpiresAt:  &expiresAt,
				LicenseNo:  "LIC-TEST",
				InstanceID: request.InstanceID,
				Sequence:   2,
				KeyID:      "test-key",
				Features:   []string{FeatureHTTP, FeatureWorker, FeaturePlugins},
				Nonce:      request.Nonce,
				IssuedAt:   time.Now().Unix(),
			}
			raw, _ := json.Marshal(payload)
			resp := heartbeatResponse{
				heartbeatSignedPayload: payload,
				Signature:              "SIGN-" + base64.RawURLEncoding.EncodeToString(ed25519.Sign(privateKey, raw)),
			}
			_ = json.NewEncoder(w).Encode(resp)
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	_, err = NewClient(server.URL, time.Second).Heartbeat(context.Background(), " inst_test ", HeartbeatRequest{
		InstanceID:      " 550e8400-e29b-41d4-a716-446655440000 ",
		ReportedVersion: " 0.1.0 ",
		BackendURL:      testBackendURL,
	})
	if err != nil {
		t.Fatalf("Heartbeat: %v", err)
	}
}

func TestClientReturnsHTTPError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "not authorized", http.StatusUnauthorized)
	}))
	defer server.Close()

	_, err := NewClient(server.URL, time.Second).Register(context.Background(), RegisterRequest{})
	var httpError *HTTPError
	if !errors.As(err, &httpError) || httpError.StatusCode != http.StatusUnauthorized {
		t.Fatalf("error = %v", err)
	}
}

func TestClientQuery(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/licenses/public-key":
			_ = json.NewEncoder(w).Encode(publicKeyResponse{
				Algorithm: "Ed25519",
				PublicKey: base64.RawURLEncoding.EncodeToString(publicKey),
				KeyID:     "test-key",
			})
		case "/api/v1/licenses/query":
			if got := r.Header.Get("Authorization"); got != "Bearer inst_test" {
				t.Fatalf("query authorization = %q", got)
			}
			var request queryRequest
			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
				t.Fatalf("decode query: %v", err)
			}
			if request.LicenseCode != "AUTH-TEST" || request.InstanceID != testInstanceID || request.OS == "" || request.CPU == "" || request.BackendURL != testBackendURL || request.Nonce == "" {
				t.Fatalf("request = %#v", request)
			}
			updateURL := "https://example.test/download"
			payload := querySignedPayload{
				Auth:            "Yes And Must",
				Status:          "active",
				Version:         "0.10",
				UpdateURL:       &updateURL,
				DBUpdate:        false,
				UpdateTime:      "2026.07.10",
				Changelog:       "None",
				ForceUpdate:     false,
				ServicePurchase: "2026.07.10",
				ServiceExpire:   "2027.07.10",
				Purchase:        "2026.07.10",
				Expire:          "None",
				License:         request.LicenseCode,
				LicenseNo:       "LIC-TEST",
				ValidUntil:      time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
				ExpiresAt:       time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339),
				Nonce:           request.Nonce,
				IssuedAt:        time.Now().Unix(),
				KeyID:           "test-key",
				InstanceID:      request.InstanceID,
				Sequence:        1,
				Features:        []string{FeatureHTTP, FeatureWorker, FeaturePlugins},
			}
			raw, err := json.Marshal(payload)
			if err != nil {
				t.Fatalf("marshal payload: %v", err)
			}
			_ = json.NewEncoder(w).Encode(queryResponse{
				querySignedPayload: payload,
				Signature:          "SIGN-" + base64.RawURLEncoding.EncodeToString(ed25519.Sign(privateKey, raw)),
			})
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	result, err := NewClient(server.URL, time.Second).Query(context.Background(), "AUTH-TEST", testInstanceID, "inst_test", testBackendURL)
	if err != nil {
		t.Fatalf("Query: %v", err)
	}
	if !result.Authorized || result.Version != "0.10" || result.LicenseNo != "LIC-TEST" {
		t.Fatalf("result = %#v", result)
	}
	if result.Expire != "" || result.Changelog != "" {
		t.Fatalf("optional values were not normalized: %#v", result)
	}
}

func TestClientQueryRejectsMismatchedNonce(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	server := signedQueryServer(t, publicKey, privateKey, func(request queryRequest) querySignedPayload {
		return querySignedPayload{
			Auth:      "Yes And Must",
			Version:   "0.10",
			License:   request.LicenseCode,
			LicenseNo: "LIC-TEST",
			Nonce:     "different_nonce",
			IssuedAt:  1783656000,
		}
	})
	defer server.Close()

	if _, err := NewClient(server.URL, time.Second).Query(context.Background(), "AUTH-TEST", testInstanceID, "inst_test", testBackendURL); err == nil {
		t.Fatal("expected nonce mismatch error")
	}
}

func TestClientQueryRejectsInvalidSignature(t *testing.T) {
	publicKey, _, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	_, otherPrivateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate other key: %v", err)
	}
	server := signedQueryServer(t, publicKey, otherPrivateKey, func(request queryRequest) querySignedPayload {
		return querySignedPayload{
			Auth:      "Yes And Must",
			Version:   "0.10",
			License:   request.LicenseCode,
			LicenseNo: "LIC-TEST",
			Nonce:     request.Nonce,
			IssuedAt:  1783656000,
		}
	})
	defer server.Close()

	if _, err := NewClient(server.URL, time.Second).Query(context.Background(), "AUTH-TEST", testInstanceID, "inst_test", testBackendURL); err == nil {
		t.Fatal("expected signature verification error")
	}
}

func TestClientQueryRejectsStaleIssuedAt(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	server := signedQueryServer(t, publicKey, privateKey, func(request queryRequest) querySignedPayload {
		payload := completeQueryPayload(request, querySignedPayload{
			Auth:      "Yes And Must",
			License:   request.LicenseCode,
			LicenseNo: "LIC-TEST",
			Nonce:     request.Nonce,
		})
		payload.IssuedAt = time.Now().Add(-time.Hour).Unix()
		return payload
	})
	defer server.Close()

	if _, err := NewClient(server.URL, time.Second).Query(context.Background(), "AUTH-TEST", testInstanceID, "inst_test", testBackendURL); !errors.Is(err, ErrLicenseProtocol) {
		t.Fatalf("error = %v", err)
	}
}

func TestClientQueryRejectsSigningKeyIDMismatch(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	server := signedQueryServer(t, publicKey, privateKey, func(request queryRequest) querySignedPayload {
		return querySignedPayload{
			Auth:       "Yes And Must",
			License:    request.LicenseCode,
			LicenseNo:  "LIC-TEST",
			Nonce:      request.Nonce,
			KeyID:      "unexpected-key",
			InstanceID: request.InstanceID,
		}
	})
	defer server.Close()

	if _, err := NewClient(server.URL, time.Second).Query(context.Background(), "AUTH-TEST", testInstanceID, "inst_test", testBackendURL); !errors.Is(err, ErrLicenseProtocol) {
		t.Fatalf("error = %v", err)
	}
}

func TestClientRejectsInsecureRemoteLicenseURL(t *testing.T) {
	client := NewClient("http://license.example.com", time.Second)
	if _, err := client.Detail(context.Background(), "AUTH-TEST"); err == nil {
		t.Fatal("expected insecure URL error")
	}
}

func TestClientDoesNotFollowLicenseRedirects(t *testing.T) {
	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("license client followed redirect")
	}))
	defer target.Close()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
	}))
	defer server.Close()

	_, err := NewClient(server.URL, time.Second).Detail(context.Background(), "AUTH-TEST")
	var httpError *HTTPError
	if !errors.As(err, &httpError) || httpError.StatusCode != http.StatusTemporaryRedirect {
		t.Fatalf("error = %v", err)
	}
}

func TestClientRequiresBuildTimePinnedKey(t *testing.T) {
	oldKey, oldID := EmbeddedPublicKeyBase64, EmbeddedPublicKeyID
	EmbeddedPublicKeyBase64, EmbeddedPublicKeyID = "", ""
	t.Cleanup(func() {
		EmbeddedPublicKeyBase64, EmbeddedPublicKeyID = oldKey, oldID
	})
	client := NewClient("http://127.0.0.1:1", time.Second).RequirePinnedKey(true)
	if _, err := client.GetPublicKey(context.Background()); err == nil {
		t.Fatal("expected missing pinned key error")
	}
}

func signedQueryServer(t *testing.T, publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, payload func(queryRequest) querySignedPayload) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/licenses/public-key":
			_ = json.NewEncoder(w).Encode(publicKeyResponse{
				Algorithm: "Ed25519",
				PublicKey: base64.RawURLEncoding.EncodeToString(publicKey),
				KeyID:     "test-key",
			})
		case "/api/v1/licenses/query":
			if got := r.Header.Get("Authorization"); got != "Bearer inst_test" {
				t.Fatalf("query authorization = %q", got)
			}
			var request queryRequest
			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
				t.Fatalf("decode query: %v", err)
			}
			responsePayload := completeQueryPayload(request, payload(request))
			raw, err := json.Marshal(responsePayload)
			if err != nil {
				t.Fatalf("marshal payload: %v", err)
			}
			_ = json.NewEncoder(w).Encode(queryResponse{
				querySignedPayload: responsePayload,
				Signature:          "SIGN-" + base64.RawURLEncoding.EncodeToString(ed25519.Sign(privateKey, raw)),
			})
		default:
			http.NotFound(w, r)
		}
	}))
}

func completeQueryPayload(request queryRequest, payload querySignedPayload) querySignedPayload {
	now := time.Now().UTC()
	if payload.Status == "" {
		payload.Status = "active"
	}
	if payload.Version == "" {
		payload.Version = "0.10"
	}
	if payload.ValidUntil == "" {
		payload.ValidUntil = now.Add(time.Hour).Format(time.RFC3339)
	}
	if payload.ExpiresAt == "" {
		payload.ExpiresAt = now.Add(24 * time.Hour).Format(time.RFC3339)
	}
	if payload.KeyID == "" {
		payload.KeyID = "test-key"
	}
	if payload.InstanceID == "" {
		payload.InstanceID = request.InstanceID
	}
	if payload.Sequence == 0 {
		payload.Sequence = 1
	}
	if len(payload.Features) == 0 {
		payload.Features = []string{FeatureHTTP, FeatureWorker, FeaturePlugins}
	}
	if payload.IssuedAt == 0 {
		payload.IssuedAt = now.Unix()
	}
	return payload
}
