package hmmm_adapter

import (
    "context"
    "errors"
    "fmt"
    "strings"
    "sync"
    "testing"
    "time"
)

func TestAdapter_Publish_OK(t *testing.T) {
    var joined, published bool
    a := NewAdapter(
        func(topic string) error { joined = (topic == "bzzz/meta/issue/42"); return nil },
        func(topic string, payload []byte) error { published = (topic == "bzzz/meta/issue/42" && len(payload) > 0); return nil },
    )
    if err := a.Publish(context.Background(), "bzzz/meta/issue/42", []byte(`{"ok":true}`)); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if !joined || !published { 
        t.Fatalf("expected join and publish to be called") 
    }
    
    // Verify metrics
    metrics := a.GetMetrics()
    if metrics.PublishCount != 1 {
        t.Fatalf("expected publish count 1, got %d", metrics.PublishCount)
    }
    if metrics.JoinCount != 1 {
        t.Fatalf("expected join count 1, got %d", metrics.JoinCount)
    }
    if metrics.ErrorCount != 0 {
        t.Fatalf("expected error count 0, got %d", metrics.ErrorCount)
    }
}

func TestAdapter_Publish_JoinError(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return errors.New("join failed") },
        func(topic string, payload []byte) error { return nil },
    )
    if err := a.Publish(context.Background(), "t", []byte("{}")); err == nil {
        t.Fatalf("expected join error")
    }
    
    // Verify error was tracked
    metrics := a.GetMetrics()
    if metrics.ErrorCount != 1 {
        t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
    }
}

func TestAdapter_Publish_PublishError(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return errors.New("publish failed") },
    )
    if err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`)); err == nil {
        t.Fatalf("expected publish error")
    }
    
    // Verify error was tracked
    metrics := a.GetMetrics()
    if metrics.ErrorCount != 1 {
        t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
    }
}

func TestAdapter_Publish_EmptyTopic(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
    )
    
    err := a.Publish(context.Background(), "", []byte(`{"test":true}`))
    if err == nil {
        t.Fatalf("expected error for empty topic")
    }
    if !strings.Contains(err.Error(), "topic cannot be empty") {
        t.Fatalf("expected empty topic error, got: %v", err)
    }
    
    metrics := a.GetMetrics()
    if metrics.ErrorCount != 1 {
        t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
    }
}

func TestAdapter_Publish_EmptyPayload(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
    )
    
    err := a.Publish(context.Background(), "test-topic", []byte{})
    if err == nil {
        t.Fatalf("expected error for empty payload")
    }
    if !strings.Contains(err.Error(), "payload cannot be empty") {
        t.Fatalf("expected empty payload error, got: %v", err)
    }
}

func TestAdapter_Publish_PayloadTooLarge(t *testing.T) {
    config := DefaultAdapterConfig()
    config.MaxPayloadSize = 10 // Very small limit for testing
    
    a := NewAdapterWithConfig(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
        config,
    )
    
    largePayload := make([]byte, 20) // Larger than limit
    err := a.Publish(context.Background(), "test-topic", largePayload)
    if err == nil {
        t.Fatalf("expected error for payload too large")
    }
    if !strings.Contains(err.Error(), "exceeds maximum") {
        t.Fatalf("expected payload size error, got: %v", err)
    }
}

func TestAdapter_Publish_TopicCaching(t *testing.T) {
    joinCallCount := 0
    a := NewAdapter(
        func(topic string) error { joinCallCount++; return nil },
        func(topic string, payload []byte) error { return nil },
    )
    
    topic := "bzzz/meta/issue/123"
    
    // First publish should join
    err := a.Publish(context.Background(), topic, []byte(`{"msg1":true}`))
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if joinCallCount != 1 {
        t.Fatalf("expected 1 join call, got %d", joinCallCount)
    }
    
    // Second publish to same topic should not join again
    err = a.Publish(context.Background(), topic, []byte(`{"msg2":true}`))
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if joinCallCount != 1 {
        t.Fatalf("expected 1 join call total, got %d", joinCallCount)
    }
    
    // Verify metrics
    metrics := a.GetMetrics()
    if metrics.JoinCount != 1 {
        t.Fatalf("expected join count 1, got %d", metrics.JoinCount)
    }
    if metrics.PublishCount != 2 {
        t.Fatalf("expected publish count 2, got %d", metrics.PublishCount)
    }
    
    // Verify topic is cached
    joinedTopics := a.GetJoinedTopics()
    if len(joinedTopics) != 1 || joinedTopics[0] != topic {
        t.Fatalf("expected topic to be cached: %v", joinedTopics)
    }
}

func TestAdapter_Publish_Timeout(t *testing.T) {
    config := DefaultAdapterConfig()
    config.PublishTimeout = 10 * time.Millisecond // Very short timeout
    
    a := NewAdapterWithConfig(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { 
            time.Sleep(50 * time.Millisecond) // Longer than timeout
            return nil 
        },
        config,
    )
    
    err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`))
    if err == nil {
        t.Fatalf("expected timeout error")
    }
    if !strings.Contains(err.Error(), "timed out") {
        t.Fatalf("expected timeout error, got: %v", err)
    }
}

func TestAdapter_Publish_JoinTimeout(t *testing.T) {
    config := DefaultAdapterConfig()
    config.JoinTimeout = 10 * time.Millisecond // Very short timeout
    
    a := NewAdapterWithConfig(
        func(topic string) error { 
            time.Sleep(50 * time.Millisecond) // Longer than timeout
            return nil 
        },
        func(topic string, payload []byte) error { return nil },
        config,
    )
    
    err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`))
    if err == nil {
        t.Fatalf("expected join timeout error")
    }
    if !strings.Contains(err.Error(), "failed to join topic") {
        t.Fatalf("expected join timeout error, got: %v", err)
    }
}

func TestAdapter_ConcurrentPublish(t *testing.T) {
    joinCalls := make(map[string]int)
    var joinMutex sync.Mutex
    
    a := NewAdapter(
        func(topic string) error {
            joinMutex.Lock()
            joinCalls[topic]++
            joinMutex.Unlock()
            return nil
        },
        func(topic string, payload []byte) error { return nil },
    )
    
    const numGoroutines = 10
    const numTopics = 3
    
    var wg sync.WaitGroup
    wg.Add(numGoroutines)
    
    for i := 0; i < numGoroutines; i++ {
        go func(id int) {
            defer wg.Done()
            topic := fmt.Sprintf("bzzz/meta/issue/%d", id%numTopics)
            payload := fmt.Sprintf(`{"id":%d}`, id)
            
            err := a.Publish(context.Background(), topic, []byte(payload))
            if err != nil {
                t.Errorf("unexpected error from goroutine %d: %v", id, err)
            }
        }(i)
    }
    
    wg.Wait()
    
    // Verify each topic was joined exactly once
    joinMutex.Lock()
    for topic, count := range joinCalls {
        if count != 1 {
            t.Errorf("topic %s was joined %d times, expected 1", topic, count)
        }
    }
    joinMutex.Unlock()
    
    // Verify metrics
    metrics := a.GetMetrics()
    if metrics.JoinCount != numTopics {
        t.Fatalf("expected join count %d, got %d", numTopics, metrics.JoinCount)
    }
    if metrics.PublishCount != numGoroutines {
        t.Fatalf("expected publish count %d, got %d", numGoroutines, metrics.PublishCount)
    }
}

func TestAdapter_ResetMetrics(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
    )
    
    // Generate some metrics
    a.Publish(context.Background(), "topic1", []byte(`{"test":true}`))
    a.Publish(context.Background(), "topic2", []byte(`{"test":true}`))
    
    metrics := a.GetMetrics()
    if metrics.PublishCount == 0 {
        t.Fatalf("expected non-zero publish count")
    }
    
    // Reset metrics
    a.ResetMetrics()
    
    metrics = a.GetMetrics()
    if metrics.PublishCount != 0 {
        t.Fatalf("expected publish count to be reset to 0, got %d", metrics.PublishCount)
    }
    if metrics.JoinCount != 0 {
        t.Fatalf("expected join count to be reset to 0, got %d", metrics.JoinCount)
    }
    if metrics.ErrorCount != 0 {
        t.Fatalf("expected error count to be reset to 0, got %d", metrics.ErrorCount)
    }
}

func TestAdapter_ClearTopicCache(t *testing.T) {
    a := NewAdapter(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
    )
    
    // Publish to create cached topics
    a.Publish(context.Background(), "topic1", []byte(`{"test":true}`))
    a.Publish(context.Background(), "topic2", []byte(`{"test":true}`))
    
    joinedTopics := a.GetJoinedTopics()
    if len(joinedTopics) != 2 {
        t.Fatalf("expected 2 joined topics, got %d", len(joinedTopics))
    }
    
    // Clear cache
    a.ClearTopicCache()
    
    joinedTopics = a.GetJoinedTopics()
    if len(joinedTopics) != 0 {
        t.Fatalf("expected 0 joined topics after cache clear, got %d", len(joinedTopics))
    }
}

func TestAdapter_DefaultConfig(t *testing.T) {
    config := DefaultAdapterConfig()
    
    if config.MaxPayloadSize <= 0 {
        t.Fatalf("expected positive max payload size, got %d", config.MaxPayloadSize)
    }
    if config.JoinTimeout <= 0 {
        t.Fatalf("expected positive join timeout, got %v", config.JoinTimeout)
    }
    if config.PublishTimeout <= 0 {
        t.Fatalf("expected positive publish timeout, got %v", config.PublishTimeout)
    }
}

func TestAdapter_CustomConfig(t *testing.T) {
    config := AdapterConfig{
        MaxPayloadSize:  1000,
        JoinTimeout:     5 * time.Second,
        PublishTimeout:  2 * time.Second,
    }
    
    a := NewAdapterWithConfig(
        func(topic string) error { return nil },
        func(topic string, payload []byte) error { return nil },
        config,
    )
    
    if a.maxPayloadSize != 1000 {
        t.Fatalf("expected max payload size 1000, got %d", a.maxPayloadSize)
    }
    if a.joinTimeout != 5*time.Second {
        t.Fatalf("expected join timeout 5s, got %v", a.joinTimeout)
    }
    if a.publishTimeout != 2*time.Second {
        t.Fatalf("expected publish timeout 2s, got %v", a.publishTimeout)
    }
}

