//go:build integration

package steme

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"
)

// Integration tests that run against a live StemeDB server.
//
// To run these tests:
//
//  1. Start the StemeDB API server:
//     cargo run --bin stemedb-api
//
//  2. Run the integration tests:
//     STEMEDB_URL=http://localhost:3000 go test -tags=integration ./...
//
// These tests are skipped by default (build tag "integration") to avoid
// breaking CI when no server is running.

// getTestClient creates a client with a generated signer for testing.
//
// Skips the test if STEMEDB_URL is not set.
func getTestClient(t *testing.T) *Client {
	t.Helper()

	baseURL := os.Getenv("STEMEDB_URL")
	if baseURL == "" {
		t.Skip("STEMEDB_URL not set - skipping integration test")
	}

	signer, err := GenerateSigner()
	if err != nil {
		t.Fatalf("GenerateSigner() failed: %v", err)
	}

	return NewClient(baseURL, signer)
}

// uniqueSubject generates a unique subject for test isolation.
//
// This prevents tests from interfering with each other by ensuring
// each test operates on distinct subjects.
func uniqueSubject(base string) string {
	return fmt.Sprintf("%s_%d", base, time.Now().UnixNano())
}

// TestIntegration_Health verifies the health endpoint works.
func TestIntegration_Health(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	health, err := client.Health(ctx)
	if err != nil {
		t.Fatalf("Health() failed: %v", err)
	}

	if health.Status != "healthy" {
		t.Errorf("Health.Status = %s, want healthy", health.Status)
	}

	if health.Version == "" {
		t.Error("Health.Version is empty")
	}

	t.Logf("Server status: %s, version: %s, assertions: %d",
		health.Status, health.Version, health.AssertionsCount)
}

// TestIntegration_Assert_Query_Roundtrip tests creating an assertion and querying it back.
func TestIntegration_Assert_Query_Roundtrip(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	// Create a unique subject to avoid interference
	subject := uniqueSubject("IntegrationTest_Entity")
	predicate := "has_value"
	expectedValue := "test_value_roundtrip"

	// Create assertion
	assertion := NewAssertion(subject, predicate).
		WithText(expectedValue).
		WithConfidence(0.95).
		WithLifecycle(LifecycleApproved).
		WithSourceClass(SourceClassClinical).
		WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
		Build()

	hash, err := client.Assert(ctx, assertion)
	if err != nil {
		t.Fatalf("Assert() failed: %v", err)
	}

	if len(hash) != 64 {
		t.Errorf("Assert() hash length = %d, want 64", len(hash))
	}

	t.Logf("Created assertion with hash: %s", hash)

	// Small delay to ensure indexing completes
	time.Sleep(100 * time.Millisecond)

	// Query it back
	params := NewQuery().
		WithSubject(subject).
		WithPredicate(predicate).
		Build()

	result, err := client.Query(ctx, params)
	if err != nil {
		t.Fatalf("Query() failed: %v", err)
	}

	if result.TotalCount == 0 {
		t.Fatal("Query() returned no results")
	}

	// Verify we got our assertion back
	found := false
	for _, a := range result.Assertions {
		if a.Hash == hash {
			found = true

			if a.Subject != subject {
				t.Errorf("Subject = %s, want %s", a.Subject, subject)
			}

			if a.Predicate != predicate {
				t.Errorf("Predicate = %s, want %s", a.Predicate, predicate)
			}

			if a.Object.Type != "Text" {
				t.Errorf("Object.Type = %s, want Text", a.Object.Type)
			}

			if a.Object.Value != expectedValue {
				t.Errorf("Object.Value = %v, want %s", a.Object.Value, expectedValue)
			}

			if a.Confidence != 0.95 {
				t.Errorf("Confidence = %f, want 0.95", a.Confidence)
			}

			if a.Lifecycle != LifecycleApproved {
				t.Errorf("Lifecycle = %s, want Approved", a.Lifecycle)
			}

			if a.SourceClass != SourceClassClinical {
				t.Errorf("SourceClass = %s, want Clinical", a.SourceClass)
			}

			if len(a.Signatures) == 0 {
				t.Error("Assertion has no signatures")
			}

			break
		}
	}

	if !found {
		t.Errorf("Did not find assertion with hash %s in query results", hash)
	}
}

// TestIntegration_Query_WithLens tests querying with different lenses.
func TestIntegration_Query_WithLens(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	// Create a unique subject for this test
	subject := uniqueSubject("IntegrationTest_Lens")
	predicate := "has_revenue"

	// Create multiple assertions with different values
	assertions := []struct {
		value      float64
		confidence float64
	}{
		{100.0, 0.9},
		{105.0, 0.95},
		{100.0, 0.85}, // Duplicate value to test consensus
	}

	for i, a := range assertions {
		assertion := NewAssertion(subject, predicate).
			WithNumber(a.value).
			WithConfidence(a.confidence).
			WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
			Build()

		hash, err := client.Assert(ctx, assertion)
		if err != nil {
			t.Fatalf("Assert(%d) failed: %v", i, err)
		}

		t.Logf("Created assertion %d with hash: %s (value: %.1f, confidence: %.2f)",
			i, hash, a.value, a.confidence)
	}

	// Small delay for indexing
	time.Sleep(100 * time.Millisecond)

	// Test different lenses
	lensTests := []struct {
		name string
		lens Lens
	}{
		{"Recency", LensRecency},
		{"Consensus", LensConsensus},
		{"Authority", LensAuthority},
	}

	for _, tt := range lensTests {
		t.Run(tt.name, func(t *testing.T) {
			params := NewQuery().
				WithSubject(subject).
				WithPredicate(predicate).
				WithLens(tt.lens).
				Build()

			result, err := client.Query(ctx, params)
			if err != nil {
				t.Fatalf("Query() with lens %s failed: %v", tt.lens, err)
			}

			if result.TotalCount == 0 {
				t.Errorf("Query() with lens %s returned no results", tt.lens)
			}

			t.Logf("Lens %s returned %d results", tt.lens, result.TotalCount)

			// Verify all results match our subject/predicate
			for _, a := range result.Assertions {
				if a.Subject != subject {
					t.Errorf("Result has wrong subject: %s, want %s", a.Subject, subject)
				}

				if a.Predicate != predicate {
					t.Errorf("Result has wrong predicate: %s, want %s", a.Predicate, predicate)
				}
			}
		})
	}
}

// TestIntegration_Query_NotFound tests querying for non-existent data.
func TestIntegration_Query_NotFound(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	// Query for a subject that definitely doesn't exist
	subject := uniqueSubject("IntegrationTest_NonExistent")

	params := NewQuery().
		WithSubject(subject).
		WithPredicate("does_not_exist").
		Build()

	result, err := client.Query(ctx, params)
	if err != nil {
		t.Fatalf("Query() failed: %v", err)
	}

	if result.TotalCount != 0 {
		t.Errorf("Query() for non-existent data returned %d results, want 0", result.TotalCount)
	}

	if len(result.Assertions) != 0 {
		t.Errorf("Query() for non-existent data returned %d assertions, want 0", len(result.Assertions))
	}

	if result.HasMore {
		t.Error("Query() for non-existent data claims HasMore = true")
	}
}

// TestIntegration_Query_Limit tests the limit parameter.
func TestIntegration_Query_Limit(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	subject := uniqueSubject("IntegrationTest_Limit")
	predicate := "has_index"

	// Create 5 assertions
	const numAssertions = 5
	for i := 0; i < numAssertions; i++ {
		assertion := NewAssertion(subject, predicate).
			WithNumber(float64(i)).
			WithConfidence(0.9).
			WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
			Build()

		_, err := client.Assert(ctx, assertion)
		if err != nil {
			t.Fatalf("Assert(%d) failed: %v", i, err)
		}
	}

	// Small delay for indexing
	time.Sleep(100 * time.Millisecond)

	// Query with limit of 2
	params := NewQuery().
		WithSubject(subject).
		WithPredicate(predicate).
		WithLimit(2).
		Build()

	result, err := client.Query(ctx, params)
	if err != nil {
		t.Fatalf("Query() failed: %v", err)
	}

	if len(result.Assertions) > 2 {
		t.Errorf("Query() with limit=2 returned %d results, want at most 2", len(result.Assertions))
	}

	if result.TotalCount > 2 && !result.HasMore {
		t.Error("Query() should indicate HasMore when total > limit")
	}
}

// TestIntegration_Assert_AllObjectTypes tests all object value types.
func TestIntegration_Assert_AllObjectTypes(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	subject := uniqueSubject("IntegrationTest_Types")

	tests := []struct {
		name      string
		predicate string
		builder   func(*AssertionBuilder) *AssertionBuilder
		checkType string
	}{
		{
			name:      "Text",
			predicate: "has_text",
			builder: func(b *AssertionBuilder) *AssertionBuilder {
				return b.WithText("hello world")
			},
			checkType: "Text",
		},
		{
			name:      "Number",
			predicate: "has_number",
			builder: func(b *AssertionBuilder) *AssertionBuilder {
				return b.WithNumber(42.5)
			},
			checkType: "Number",
		},
		{
			name:      "Boolean",
			predicate: "has_boolean",
			builder: func(b *AssertionBuilder) *AssertionBuilder {
				return b.WithBoolean(true)
			},
			checkType: "Boolean",
		},
		{
			name:      "Reference",
			predicate: "has_reference",
			builder: func(b *AssertionBuilder) *AssertionBuilder {
				return b.WithReference("Entity_123")
			},
			checkType: "Reference",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			builder := NewAssertion(subject, tt.predicate).
				WithConfidence(0.9).
				WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000")

			builder = tt.builder(builder)
			assertion := builder.Build()

			hash, err := client.Assert(ctx, assertion)
			if err != nil {
				t.Fatalf("Assert() for %s failed: %v", tt.name, err)
			}

			t.Logf("Created %s assertion with hash: %s", tt.name, hash)

			// Small delay for indexing
			time.Sleep(100 * time.Millisecond)

			// Query it back
			params := NewQuery().
				WithSubject(subject).
				WithPredicate(tt.predicate).
				Build()

			result, err := client.Query(ctx, params)
			if err != nil {
				t.Fatalf("Query() for %s failed: %v", tt.name, err)
			}

			if result.TotalCount == 0 {
				t.Fatalf("Query() for %s returned no results", tt.name)
			}

			// Verify object type
			if result.Assertions[0].Object.Type != tt.checkType {
				t.Errorf("Object.Type = %s, want %s", result.Assertions[0].Object.Type, tt.checkType)
			}
		})
	}
}

// TestIntegration_Assert_InvalidAssertion tests that invalid assertions are rejected.
func TestIntegration_Assert_InvalidAssertion(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	tests := []struct {
		name      string
		assertion Assertion
		wantErr   bool
	}{
		{
			name: "missing source_hash",
			assertion: NewAssertion("Subject", "Predicate").
				WithText("value").
				Build(),
			wantErr: true,
		},
		{
			name: "invalid confidence",
			assertion: NewAssertion("Subject", "Predicate").
				WithText("value").
				WithConfidence(1.5).
				WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
				Build(),
			wantErr: true,
		},
		{
			name: "negative confidence",
			assertion: NewAssertion("Subject", "Predicate").
				WithText("value").
				WithConfidence(-0.1).
				WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
				Build(),
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := client.Assert(ctx, tt.assertion)
			if (err != nil) != tt.wantErr {
				t.Errorf("Assert() error = %v, wantErr %v", err, tt.wantErr)
			}

			if tt.wantErr && err != nil {
				t.Logf("Got expected error: %v", err)
			}
		})
	}
}

// TestIntegration_Lifecycle tests lifecycle stage filtering.
func TestIntegration_Lifecycle(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	subject := uniqueSubject("IntegrationTest_Lifecycle")
	predicate := "has_status"

	// Create assertions with different lifecycle stages
	stages := []LifecycleStage{
		LifecycleProposed,
		LifecycleApproved,
		LifecycleDeprecated,
	}

	for _, stage := range stages {
		assertion := NewAssertion(subject, predicate).
			WithText(string(stage)).
			WithLifecycle(stage).
			WithConfidence(0.9).
			WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
			Build()

		hash, err := client.Assert(ctx, assertion)
		if err != nil {
			t.Fatalf("Assert() for lifecycle %s failed: %v", stage, err)
		}

		t.Logf("Created assertion with lifecycle %s: %s", stage, hash)
	}

	// Small delay for indexing
	time.Sleep(100 * time.Millisecond)

	// Query for only Approved lifecycle
	params := NewQuery().
		WithSubject(subject).
		WithPredicate(predicate).
		WithLifecycle(LifecycleApproved).
		Build()

	result, err := client.Query(ctx, params)
	if err != nil {
		t.Fatalf("Query() failed: %v", err)
	}

	// Should only get the Approved assertion
	for _, a := range result.Assertions {
		if a.Lifecycle != LifecycleApproved {
			t.Errorf("Got assertion with lifecycle %s, want Approved only", a.Lifecycle)
		}
	}

	t.Logf("Lifecycle filtering returned %d Approved assertions", result.TotalCount)
}

// TestIntegration_Trace tests the trace endpoint for audit trail querying.
func TestIntegration_Trace(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	// Create a unique subject for this test
	subject := uniqueSubject("IntegrationTest_Trace")
	predicate := "has_trace_value"

	// Create an assertion to generate query activity
	assertion := NewAssertion(subject, predicate).
		WithText("test_trace_value").
		WithConfidence(0.95).
		WithLifecycle(LifecycleApproved).
		WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
		Build()

	hash, err := client.Assert(ctx, assertion)
	if err != nil {
		t.Fatalf("Assert() failed: %v", err)
	}

	t.Logf("Created assertion with hash: %s", hash)

	// Small delay for indexing
	time.Sleep(100 * time.Millisecond)

	// Perform a query to create audit trail
	params := NewQuery().
		WithSubject(subject).
		WithPredicate(predicate).
		Build()

	queryResult, err := client.Query(ctx, params)
	if err != nil {
		t.Fatalf("Query() failed: %v", err)
	}

	if queryResult.TotalCount == 0 {
		t.Fatal("Query() returned no results")
	}

	t.Logf("Query returned %d results", queryResult.TotalCount)

	// Small delay for audit to be written
	time.Sleep(100 * time.Millisecond)

	// Get the agent ID from the client's signer
	agentID := client.signer.PublicKey()

	// Calculate time range (last 1 hour to now)
	now := time.Now().Unix()
	from := now - 3600

	// Trace the agent's queries
	traceParams := TraceParams{
		AgentID: agentID,
		From:    fmt.Sprintf("%d", from),
		To:      fmt.Sprintf("%d", now),
		Subject: subject, // Filter to our test subject
		Limit:   100,
	}

	traceResult, err := client.Trace(ctx, traceParams)
	if err != nil {
		t.Fatalf("Trace() failed: %v", err)
	}

	t.Logf("Trace returned %d audit records", traceResult.TotalCount)

	// Verify we got at least one audit record
	if traceResult.TotalCount == 0 {
		t.Error("Trace() returned no audit records")
	}

	// Verify the trace result has the correct agent ID
	if traceResult.AgentID != agentID {
		t.Errorf("TraceResult.AgentID = %s, want %s", traceResult.AgentID, agentID)
	}

	// Verify the trace result has the correct time range
	if traceResult.FromTimestamp != uint64(from) {
		t.Errorf("TraceResult.FromTimestamp = %d, want %d", traceResult.FromTimestamp, from)
	}

	// Verify audit records contain our query
	foundOurQuery := false
	for _, audit := range traceResult.Audits {
		t.Logf("Audit record: query_id=%s, timestamp=%d, subject=%v, predicate=%v",
			audit.QueryID, audit.Timestamp, audit.Params.Subject, audit.Params.Predicate)

		// Check if this audit is for our query
		if audit.Params.Subject != nil && *audit.Params.Subject == subject &&
			audit.Params.Predicate != nil && *audit.Params.Predicate == predicate {
			foundOurQuery = true

			// Verify agent ID
			if audit.AgentID == nil || *audit.AgentID != agentID {
				t.Errorf("Audit.AgentID = %v, want %s", audit.AgentID, agentID)
			}

			// Verify result hash is present
			if audit.ResultHash == nil {
				t.Error("Audit.ResultHash is nil, expected a result")
			}

			// Verify contributing assertions
			if len(audit.ContributingAssertions) == 0 {
				t.Error("Audit.ContributingAssertions is empty, expected at least one")
			}
		}
	}

	if !foundOurQuery {
		t.Errorf("Did not find audit record for our query (subject=%s, predicate=%s)", subject, predicate)
	}
}

// TestIntegration_Trace_WithPattern tests trace with subject pattern filtering.
func TestIntegration_Trace_WithPattern(t *testing.T) {
	client := getTestClient(t)
	ctx := context.Background()

	// Create unique subjects with a common prefix
	prefix := uniqueSubject("IntegrationTest_TracePattern")
	subject1 := prefix + "_A"
	subject2 := prefix + "_B"
	predicate := "has_value"

	// Create assertions and query them
	for _, subj := range []string{subject1, subject2} {
		assertion := NewAssertion(subj, predicate).
			WithText("test_value").
			WithConfidence(0.9).
			WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000").
			Build()

		_, err := client.Assert(ctx, assertion)
		if err != nil {
			t.Fatalf("Assert() for %s failed: %v", subj, err)
		}

		time.Sleep(50 * time.Millisecond)

		// Query to create audit trail
		params := NewQuery().WithSubject(subj).WithPredicate(predicate).Build()
		_, err = client.Query(ctx, params)
		if err != nil {
			t.Fatalf("Query() for %s failed: %v", subj, err)
		}
	}

	time.Sleep(100 * time.Millisecond)

	// Trace with wildcard pattern
	agentID := client.signer.PublicKey()
	now := time.Now().Unix()
	from := now - 3600

	traceParams := TraceParams{
		AgentID: agentID,
		From:    fmt.Sprintf("%d", from),
		To:      fmt.Sprintf("%d", now),
		Subject: prefix + "*", // Match both subjects
		Limit:   100,
	}

	traceResult, err := client.Trace(ctx, traceParams)
	if err != nil {
		t.Fatalf("Trace() failed: %v", err)
	}

	t.Logf("Trace with pattern returned %d audit records", traceResult.TotalCount)

	// Should find both queries
	foundSubjects := make(map[string]bool)
	for _, audit := range traceResult.Audits {
		if audit.Params.Subject != nil {
			foundSubjects[*audit.Params.Subject] = true
		}
	}

	if !foundSubjects[subject1] {
		t.Errorf("Did not find audit for subject %s", subject1)
	}

	if !foundSubjects[subject2] {
		t.Errorf("Did not find audit for subject %s", subject2)
	}
}
