// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0

package atom

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

func TestUpsertResourceCreatesThenUpdatesOnConflict(t *testing.T) {
	var operations []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		body, _ := io.ReadAll(r.Body)
		payload := string(body)
		switch {
		case strings.Contains(payload, "createResource"):
			operations = append(operations, "createResource")
			_ = json.NewEncoder(w).Encode(map[string]any{
				"errors": []map[string]string{{"message": "duplicate key value violates unique constraint"}},
			})
			return
		case strings.Contains(payload, "updateResource"):
			operations = append(operations, "updateResource")
			_ = json.NewEncoder(w).Encode(map[string]any{
				"data": map[string]any{
					"updateResource": map[string]any{"id": "res-1", "kind": KindChannel, "name": "ch"},
				},
			})
			return
		default:
			t.Fatalf("unexpected GraphQL payload: %s", payload)
		}
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	if err := client.UpsertResource(context.Background(), Resource{ID: "res-1", Kind: KindChannel, Name: "ch"}); err != nil {
		t.Fatalf("upsert failed: %v", err)
	}

	want := []string{"createResource", "updateResource"}
	if len(operations) != len(want) {
		t.Fatalf("unexpected operation count: got %v want %v", operations, want)
	}
	for i := range want {
		if operations[i] != want[i] {
			t.Fatalf("unexpected operations: got %v want %v", operations, want)
		}
	}
}

func TestListResources(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if payload.Variables["kind"] != KindRule || payload.Variables["tenantId"] != testDomainID {
			t.Fatalf("unexpected variables: %+v", payload.Variables)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"resources": map[string]any{
					"items": []Resource{{ID: "rule-1", Kind: KindRule, Name: "high-temp"}},
					"total": 1,
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.ListResources(context.Background(), Query{Kind: KindRule, TenantID: testDomainID})
	if err != nil {
		t.Fatalf("list failed: %v", err)
	}
	if got.Total != 1 || got.Items[0].ID != "rule-1" {
		t.Fatalf("unexpected list: %+v", got)
	}
}

func TestCurrentAtomCompatibilitySurface(t *testing.T) {
	const (
		serviceToken = "service-token"
		runtimeToken = "runtime-token"
	)

	seen := map[string]bool{}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case atomGraphQLPath:
			if r.Method != http.MethodPost {
				t.Fatalf("unexpected GraphQL method: %s", r.Method)
			}
			if got := r.Header.Get("Authorization"); got != "Bearer "+serviceToken {
				t.Fatalf("unexpected GraphQL authorization header: %q", got)
			}
			var payload struct {
				Query     string         `json:"query"`
				Variables map[string]any `json:"variables"`
			}
			if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
				t.Fatalf("decode request: %v", err)
			}
			switch {
			case strings.Contains(payload.Query, "authzCheck"):
				seen["authzCheck"] = true
				input, ok := payload.Variables["input"].(map[string]any)
				if !ok {
					t.Fatalf("unexpected authz input: %+v", payload.Variables["input"])
				}
				if input["subjectId"] != testEntityID || input["action"] != atomActionPublish || input["resourceId"] != "channel-1" {
					t.Fatalf("unexpected authz input: %+v", input)
				}
				_ = json.NewEncoder(w).Encode(map[string]any{
					"data": map[string]any{
						"authzCheck": map[string]any{"allowed": true, "reason": "allowed"},
					},
				})
			case strings.Contains(payload.Query, "authorizedObjectIds"):
				seen["authorizedObjectIds"] = true
				input, ok := payload.Variables["input"].(map[string]any)
				if !ok {
					t.Fatalf("unexpected authorized objects input: %+v", payload.Variables["input"])
				}
				if input["subjectId"] != testEntityID ||
					input["action"] != atomActionRead ||
					input["objectKind"] != atomObjectKindEntity ||
					input["objectType"] != atomKindDevice ||
					input["tenantId"] != testDomainID {
					t.Fatalf("unexpected authorized objects input: %+v", input)
				}
				_ = json.NewEncoder(w).Encode(map[string]any{
					"data": map[string]any{
						"authorizedObjectIds": map[string]any{"ids": []string{testDeviceID}, "total": 1},
					},
				})
			case strings.Contains(payload.Query, "createAccessToken"):
				seen["createAccessToken"] = true
				input, ok := payload.Variables["input"].(map[string]any)
				if !ok ||
					input["name"] != "magistrala-service" ||
					input["description"] != "Magistrala service token" ||
					input["subjectId"] != testEntityID ||
					input["scoped"] != false ||
					len(input["permissions"].([]any)) != 0 {
					t.Fatalf("unexpected createAccessToken input: %+v", payload.Variables["input"])
				}
				_ = json.NewEncoder(w).Encode(map[string]any{
					"data": map[string]any{
						"createAccessToken": map[string]any{
							"credentialId": testCredentialID,
							"token":        "atom_00000000000000000000000000000000_0000000000000000000000000000000000000000000000000000000000000000",
							"name":         "magistrala-service",
						},
					},
				})
			default:
				t.Fatalf("unexpected GraphQL payload: %s", payload.Query)
			}
		case atomAuthIntrospectPath:
			seen["introspect"] = true
			if r.Method != http.MethodGet {
				t.Fatalf("unexpected introspection method: %s", r.Method)
			}
			if got := r.Header.Get("Authorization"); got != "Bearer "+runtimeToken {
				t.Fatalf("unexpected introspection authorization header: %q", got)
			}
			_ = json.NewEncoder(w).Encode(IntrospectionResponse{
				Active:    true,
				EntityID:  testEntityID,
				TenantID:  testDomainID,
				SessionID: "session-1",
			})
		default:
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Token: serviceToken, Timeout: time.Second})
	authz, err := client.CheckAuthz(context.Background(), AuthzRequest{
		SubjectID:  testEntityID,
		Action:     atomActionPublish,
		ResourceID: "channel-1",
	})
	if err != nil {
		t.Fatalf("authz check failed: %v", err)
	}
	if !authz.Allowed {
		t.Fatalf("unexpected authz response: %+v", authz)
	}

	objects, err := client.AuthorizedObjectIDs(context.Background(), AuthorizedObjectIDsQuery{
		SubjectID:  testEntityID,
		Action:     atomActionRead,
		ObjectKind: atomObjectKindEntity,
		ObjectType: atomKindDevice,
		TenantID:   testDomainID,
		Limit:      10,
	})
	if err != nil {
		t.Fatalf("authorized object listing failed: %v", err)
	}
	if objects.Total != 1 || len(objects.IDs) != 1 || objects.IDs[0] != testDeviceID {
		t.Fatalf("unexpected authorized object listing: %+v", objects)
	}

	created, err := client.CreateUnscopedAccessToken(context.Background(), testEntityID, "magistrala-service", "Magistrala service token")
	if err != nil {
		t.Fatalf("create access token failed: %v", err)
	}
	if created.CredentialID != testCredentialID || created.Token == "" {
		t.Fatalf("unexpected access token response: %+v", created)
	}

	introspection, err := client.Introspect(context.Background(), runtimeToken)
	if err != nil {
		t.Fatalf("introspection failed: %v", err)
	}
	if !introspection.Active || introspection.EntityID != testEntityID || introspection.TenantID != testDomainID {
		t.Fatalf("unexpected introspection response: %+v", introspection)
	}

	for _, operation := range []string{"authzCheck", "authorizedObjectIds", "createAccessToken", "introspect"} {
		if !seen[operation] {
			t.Fatalf("operation %q was not exercised", operation)
		}
	}
}

func TestCreateTenantMapsRouteToAlias(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "createTenant") || !strings.Contains(payload.Query, "route: alias") {
			t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
		}
		input, ok := payload.Variables["input"].(map[string]any)
		if !ok {
			t.Fatalf("unexpected input: %+v", payload.Variables["input"])
		}
		if input["alias"] != "d1" {
			t.Fatalf("expected alias input from route, got: %+v", input)
		}
		if _, ok := input["route"]; ok {
			t.Fatalf("input must not use Atom route field: %+v", input)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"createTenant": map[string]any{
					"id":     testTenantID,
					"name":   "D1",
					"route":  "d1",
					"status": "active",
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.CreateTenant(context.Background(), Tenant{Name: "D1", Route: "d1"})
	if err != nil {
		t.Fatalf("create tenant failed: %v", err)
	}
	if got.ID != testTenantID || got.Route != "d1" {
		t.Fatalf("unexpected tenant: %+v", got)
	}
}

func TestUpdateTenantMapsRouteToAlias(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "updateTenant") || !strings.Contains(payload.Query, "route: alias") {
			t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
		}
		if payload.Variables["id"] != testTenantID {
			t.Fatalf("unexpected variables: %+v", payload.Variables)
		}
		input, ok := payload.Variables["input"].(map[string]any)
		if !ok {
			t.Fatalf("unexpected input: %+v", payload.Variables["input"])
		}
		if input["alias"] != "d1" {
			t.Fatalf("expected alias input from route, got: %+v", input)
		}
		if _, ok := input["route"]; ok {
			t.Fatalf("input must not use Atom route field: %+v", input)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"updateTenant": map[string]any{
					"id":     testTenantID,
					"name":   "D1",
					"route":  "d1",
					"status": "active",
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.UpdateTenant(context.Background(), testTenantID, Tenant{Name: "D1", Route: "d1"})
	if err != nil {
		t.Fatalf("update tenant failed: %v", err)
	}
	if got.ID != testTenantID || got.Route != "d1" {
		t.Fatalf("unexpected tenant: %+v", got)
	}
}

func TestListTenantsMapsRouteToAlias(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "$alias: String") ||
			!strings.Contains(payload.Query, "alias: $alias") ||
			!strings.Contains(payload.Query, "route: alias") {
			t.Fatalf("query does not use Atom alias for tenant route lookup: %s", payload.Query)
		}
		if strings.Contains(payload.Query, "$route") || strings.Contains(payload.Query, "route: $route") {
			t.Fatalf("query must not use removed Atom route field/filter: %s", payload.Query)
		}
		if payload.Variables["alias"] != "d1" {
			t.Fatalf("expected alias variable from route, got: %+v", payload.Variables)
		}
		if _, ok := payload.Variables["route"]; ok {
			t.Fatalf("variables must not use Atom route field: %+v", payload.Variables)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"tenants": map[string]any{
					"items": []Tenant{{ID: testTenantID, Name: "D1", Route: "d1", Status: "active"}},
					"total": 1,
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.ListTenants(context.Background(), Query{Route: "d1", Limit: 1})
	if err != nil {
		t.Fatalf("list tenants failed: %v", err)
	}
	if got.Total != 1 || len(got.Items) != 1 || got.Items[0].Route != "d1" {
		t.Fatalf("unexpected tenants: %+v", got)
	}
}

func TestCreateSharedKey(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "createSharedKey") {
			t.Fatalf("query does not create shared key: %s", payload.Query)
		}
		if payload.Variables["entityId"] != testDeviceID {
			t.Fatalf("unexpected entity id: %+v", payload.Variables)
		}
		input, ok := payload.Variables["input"].(map[string]any)
		if !ok {
			t.Fatalf("unexpected input: %+v", payload.Variables["input"])
		}
		if input["key"] != testClientKey || input["description"] != "provisioned from mg" {
			t.Fatalf("unexpected input: %+v", input)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"createSharedKey": map[string]any{
					"credentialId": testCredentialID,
					"key":          testClientKey,
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.CreateSharedKey(context.Background(), testDeviceID, testClientKey, "provisioned from mg")
	if err != nil {
		t.Fatalf("create shared key failed: %v", err)
	}
	if got.CredentialID != testCredentialID || got.Key != testClientKey {
		t.Fatalf("unexpected shared key response: %+v", got)
	}
}

func TestRevealSharedKey(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "revealSharedKey") {
			t.Fatalf("query does not reveal shared key: %s", payload.Query)
		}
		if payload.Variables["entityId"] != testDeviceID || payload.Variables["credentialId"] != testCredentialID {
			t.Fatalf("unexpected variables: %+v", payload.Variables)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"revealSharedKey": map[string]any{
					"credentialId": testCredentialID,
					"key":          testClientKey,
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.RevealSharedKey(context.Background(), testDeviceID, testCredentialID)
	if err != nil {
		t.Fatalf("reveal shared key failed: %v", err)
	}
	if got.CredentialID != testCredentialID || got.Key != testClientKey {
		t.Fatalf("unexpected shared key response: %+v", got)
	}
}

func TestListCredentials(t *testing.T) {
	createdAt := "2026-06-30T10:15:30Z"
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
			t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
		}
		var payload struct {
			Query     string         `json:"query"`
			Variables map[string]any `json:"variables"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if !strings.Contains(payload.Query, "credentials") {
			t.Fatalf("query does not list credentials: %s", payload.Query)
		}
		if payload.Variables["entityId"] != testDeviceID {
			t.Fatalf("unexpected variables: %+v", payload.Variables)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"credentials": map[string]any{
					"total": 1,
					"items": []map[string]any{{
						"id":         testCredentialID,
						"entity_id":  testDeviceID,
						"kind":       "shared_key",
						"identifier": "",
						"status":     "active",
						"created_at": createdAt,
					}},
				},
			},
		})
	}))
	defer srv.Close()

	client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
	got, err := client.ListCredentials(context.Background(), testDeviceID)
	if err != nil {
		t.Fatalf("list credentials failed: %v", err)
	}
	if got.Total != 1 || len(got.Items) != 1 {
		t.Fatalf("unexpected credentials response: %+v", got)
	}
	item := got.Items[0]
	if item.ID != testCredentialID || item.EntityID != testDeviceID || item.Kind != "shared_key" || item.Status != "active" {
		t.Fatalf("unexpected credential item: %+v", item)
	}
	if item.CreatedAt.Format(time.RFC3339) != createdAt {
		t.Fatalf("unexpected created_at: %s", item.CreatedAt.Format(time.RFC3339))
	}
}

func TestLoadConfig(t *testing.T) {
	t.Setenv("ATOM_URL", "http://atom:8080/")
	t.Setenv("ATOM_ADMIN_TOKEN", "token")
	t.Setenv("ATOM_ADMIN_USERNAME", "admin")
	t.Setenv("ATOM_ADMIN_SECRET", "secret")
	t.Setenv("ATOM_TIMEOUT", "3s")

	cfg := LoadConfig()
	if cfg.URL != "http://atom:8080" || cfg.JWKSURL != "http://atom:8080/.well-known/jwks.json" || cfg.Token != "token" || cfg.AdminUsername != "admin" || cfg.AdminSecret != "secret" {
		t.Fatalf("unexpected config: %+v", cfg)
	}
	if cfg.Timeout != 3*time.Second {
		t.Fatalf("unexpected timeout: %s", cfg.Timeout)
	}
}
