package redis_test

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"testing"

	miniredis "github.com/alicebob/miniredis/v2"

	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/config"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/events"
	redisclient "bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/redis"
)

type publisherFixture struct {
	server    *miniredis.Miniredis
	client    *redisclient.Client
	publisher *redisclient.Publisher
}

func newPublisherFixture(t *testing.T, redisCfg config.RedisConfig) publisherFixture {
	t.Helper()

	server, err := miniredis.Run()
	if err != nil {
		t.Fatalf("start miniredis: %v", err)
	}

	if redisCfg.URL == "" {
		redisCfg.URL = "redis://" + server.Addr()
	}

	client, err := redisclient.New(redisCfg)
	if err != nil {
		server.Close()
		t.Fatalf("new client: %v", err)
	}

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	publisher := redisclient.NewPublisher(client, redisCfg, logger)

	return publisherFixture{
		server:    server,
		client:    client,
		publisher: publisher,
	}
}

func (f publisherFixture) close() {
	f.client.Close()
	f.server.Close()
}

func streamPayload(t *testing.T, client *redisclient.Client, stream string) string {
	t.Helper()

	msgs, err := client.Underlying().XRange(context.Background(), stream, "-", "+").Result()
	if err != nil {
		t.Fatalf("xrange %s: %v", stream, err)
	}
	if len(msgs) != 1 {
		t.Fatalf("expected 1 message on %s, got %d", stream, len(msgs))
	}

	payload, ok := msgs[0].Values["payload"].(string)
	if !ok {
		t.Fatalf("expected string payload on %s", stream)
	}
	return payload
}

func streamLength(t *testing.T, server *miniredis.Miniredis, stream string) int {
	t.Helper()
	entries, err := server.Stream(stream)
	if err != nil {
		return 0
	}
	return len(entries)
}

func TestPublishFileChangedValid(t *testing.T) {
	fix := newPublisherFixture(t, config.RedisConfig{})
	defer fix.close()

	event := events.NewFileChangedEvent(
		"evt_1",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"internal/config/config.go",
		"go",
		events.FileKindCode,
		events.ChangeTypeAdded,
	)

	if err := fix.publisher.PublishFileChanged(context.Background(), event); err != nil {
		t.Fatalf("publish: %v", err)
	}

	payload := streamPayload(t, fix.client, events.StreamFilesChanged)

	var decoded events.FileChangedEvent
	if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if decoded.EventID != event.EventID {
		t.Fatalf("event_id: got %q, want %q", decoded.EventID, event.EventID)
	}
	if decoded.FileKind != events.FileKindCode {
		t.Fatalf("file_kind: got %q, want code", decoded.FileKind)
	}
}

func TestPublishRepoSnapshotReadyValid(t *testing.T) {
	fix := newPublisherFixture(t, config.RedisConfig{})
	defer fix.close()

	event := events.NewRepoSnapshotReadyEvent(
		"evt_2",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"main",
	)

	if err := fix.publisher.PublishRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("publish: %v", err)
	}

	payload := streamPayload(t, fix.client, events.StreamRepoSnapshotReady)

	var decoded events.RepoSnapshotReadyEvent
	if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if decoded.Ref != "main" {
		t.Fatalf("ref: got %q, want main", decoded.Ref)
	}
}

func TestPublishCommitsChangedValid(t *testing.T) {
	fix := newPublisherFixture(t, config.RedisConfig{})
	defer fix.close()

	event := events.NewCommitsChangedEvent(
		"evt_3",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
	)

	if err := fix.publisher.PublishCommitsChanged(context.Background(), event); err != nil {
		t.Fatalf("publish: %v", err)
	}

	payload := streamPayload(t, fix.client, events.StreamCommitsChanged)

	var decoded events.CommitsChangedEvent
	if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if decoded.CommitSHA != event.CommitSHA {
		t.Fatalf("commit_sha: got %q, want %q", decoded.CommitSHA, event.CommitSHA)
	}
}

func TestPublishFileChangedInvalid(t *testing.T) {
	fix := newPublisherFixture(t, config.RedisConfig{})
	defer fix.close()

	event := events.FileChangedEvent{
		RepoID:     "company/payment-service",
		SnapshotID: "snap_1",
		CommitSHA:  "a3f9c12b",
		FilePath:   "README.md",
		Language:   "markdown",
		ChangeType: events.ChangeTypeAdded,
	}

	if err := fix.publisher.PublishFileChanged(context.Background(), event); err == nil {
		t.Fatal("expected validation error")
	}
	if streamLength(t, fix.server, events.StreamFilesChanged) != 0 {
		t.Fatal("expected no messages on stream after validation failure")
	}
}

func TestPublishWithStreamPrefix(t *testing.T) {
	redisCfg := config.RedisConfig{StreamPrefix: "staging"}
	fix := newPublisherFixture(t, redisCfg)
	defer fix.close()

	event := events.NewFileChangedEvent(
		"evt_4",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"docs/architecture.md",
		"markdown",
		events.FileKindDocs,
		events.ChangeTypeAdded,
	)

	if err := fix.publisher.PublishFileChanged(context.Background(), event); err != nil {
		t.Fatalf("publish: %v", err)
	}

	stream := redisCfg.StreamName(events.StreamFilesChanged)
	if stream != "staging.files.changed" {
		t.Fatalf("unexpected stream name: %s", stream)
	}

	payload := streamPayload(t, fix.client, stream)

	var decoded events.FileChangedEvent
	if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if decoded.FileKind != events.FileKindDocs {
		t.Fatalf("file_kind: got %q, want docs", decoded.FileKind)
	}
}
