package redis_test

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

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

	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/config"
	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
	redisclient "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.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 TestPublishChunksReadyValid(t *testing.T) {
	fix := newPublisherFixture(t, config.RedisConfig{})
	defer fix.close()

	event := contractevents.NewChunksReadyEvent(
		"evt_4",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"src/payments/processor.go",
		"go",
		[]contractevents.CodeChunk{
			{ChunkID: "c1", SymbolName: "Process", SymbolType: contractevents.SymbolTypeFunction, StartLine: 1, EndLine: 10},
		},
	)

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

	payload := streamPayload(t, fix.client, contractevents.StreamChunksReady)

	var decoded contractevents.ChunksReadyEvent
	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.FilePath != event.FilePath {
		t.Fatalf("file_path: got %q, want %q", decoded.FilePath, event.FilePath)
	}
}

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

	event := contractevents.NewGraphArtifactReadyEvent(
		"evt_5",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"art_1",
		"file:///data/artifacts/art_1.json",
		"v0.1.0",
		graph.SchemaVersionV1,
	)

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

	payload := streamPayload(t, fix.client, contractevents.StreamGraphArtifactReady)

	var decoded contractevents.GraphArtifactReadyEvent
	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.ArtifactID != event.ArtifactID {
		t.Fatalf("artifact_id: got %q, want %q", decoded.ArtifactID, event.ArtifactID)
	}
}

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

	event := contractevents.NewParserDiagnosticsReadyEvent(
		"evt_7",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"job_1",
		[]contractevents.ParserDiagnostic{
			{
				Code:     "PARSE_ERROR",
				Message:  "unexpected token",
				Severity: contractevents.DiagnosticSeverityError,
				FilePath: "main.go",
				Line:     42,
			},
		},
	)

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

	payload := streamPayload(t, fix.client, contractevents.StreamParserDiagnostics)

	var decoded contractevents.ParserDiagnosticsReadyEvent
	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.JobID != event.JobID {
		t.Fatalf("job_id: got %q, want %q", decoded.JobID, event.JobID)
	}
}

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

	event := contractevents.NewGraphDeltaReadyEvent(
		"evt_6",
		"company/payment-service",
		"b2e8a01a",
		"a3f9c12b",
		"delta_1",
		"file:///data/artifacts/delta_1.json",
		graph.SchemaVersionV1,
	)

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

	payload := streamPayload(t, fix.client, contractevents.StreamGraphDeltaReady)

	var decoded contractevents.GraphDeltaReadyEvent
	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.DeltaArtifactID != event.DeltaArtifactID {
		t.Fatalf("delta_artifact_id: got %q, want %q", decoded.DeltaArtifactID, event.DeltaArtifactID)
	}
}

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

	event := contractevents.NewGraphArtifactReadyEvent(
		"evt_5",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"art_1",
		"",
		"v0.1.0",
		graph.SchemaVersionV1,
	)

	if err := fix.publisher.PublishGraphArtifactReady(context.Background(), event); err == nil {
		t.Fatal("expected validation error")
	}
	if streamLength(t, fix.server, contractevents.StreamGraphArtifactReady) != 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 := contractevents.NewChunksReadyEvent(
		"evt_8",
		"company/payment-service",
		"snap_1",
		"a3f9c12b",
		"main.go",
		"go",
		[]contractevents.CodeChunk{
			{ChunkID: "c1", SymbolName: "main", SymbolType: contractevents.SymbolTypeFunction, StartLine: 1, EndLine: 5},
		},
	)

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

	stream := redisCfg.StreamName(contractevents.StreamChunksReady)
	if stream != "staging.chunks.ready" {
		t.Fatalf("unexpected stream name: %s", stream)
	}

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

	var decoded contractevents.ChunksReadyEvent
	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)
	}
}
