package docker

import (
	"bytes"
	"context"
	"encoding/json"
	"sync"
	"testing"

	configmocks "github.com/schmitthub/clawker/internal/config/mocks"
	"github.com/schmitthub/clawker/internal/logger"
	"github.com/schmitthub/clawker/pkg/whail"
	"github.com/schmitthub/clawker/pkg/whail/whailtest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var progressCfg = configmocks.NewBlankConfig()

// eventCollector collects BuildProgressEvents in a thread-safe manner.
type eventCollector struct {
	mu     sync.Mutex
	events []whail.BuildProgressEvent
}

func (c *eventCollector) collect(event whail.BuildProgressEvent) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.events = append(c.events, event)
}

func (c *eventCollector) all() []whail.BuildProgressEvent {
	c.mu.Lock()
	defer c.mu.Unlock()
	return append([]whail.BuildProgressEvent{}, c.events...)
}

func buildLegacyStream(events ...buildEvent) []byte {
	var buf bytes.Buffer
	for _, e := range events {
		data, _ := json.Marshal(e)
		buf.Write(data)
		buf.WriteByte('\n')
	}
	return buf.Bytes()
}

func TestProcessBuildOutputWithProgress_StepParsing(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/3 : FROM node:20-slim\n"},
		buildEvent{Stream: " ---> abc123\n"},
		buildEvent{Stream: "Step 2/3 : RUN apt-get update\n"},
		buildEvent{Stream: " ---> Running in def456\n"},
		buildEvent{Stream: "reading package lists...\n"},
		buildEvent{Stream: "Step 3/3 : COPY . /app\n"},
		buildEvent{Stream: " ---> ghi789\n"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.NoError(t, err)

	events := collector.all()

	// Find step status events (non-log events)
	var steps []whail.BuildProgressEvent
	for _, e := range events {
		if e.LogLine == "" {
			steps = append(steps, e)
		}
	}

	// Should have: running(0), complete(0), running(1), complete(1), running(2), complete(2)
	require.GreaterOrEqual(t, len(steps), 6, "expected at least 6 step events, got %d", len(steps))

	// First step starts running
	assert.Equal(t, "step-0", steps[0].StepID)
	assert.Equal(t, whail.BuildStepRunning, steps[0].Status)
	assert.Equal(t, "FROM node:20-slim", steps[0].StepName)
	assert.Equal(t, 0, steps[0].StepIndex)
	assert.Equal(t, 3, steps[0].TotalSteps)
}

func TestProcessBuildOutputWithProgress_CacheHit(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/2 : FROM node:20-slim\n"},
		buildEvent{Stream: " ---> Using cache\n"},
		buildEvent{Stream: "Step 2/2 : RUN echo hello\n"},
		buildEvent{Stream: "hello\n"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.NoError(t, err)

	events := collector.all()

	// Find the cached event
	var cached bool
	for _, e := range events {
		if e.Status == whail.BuildStepCached {
			cached = true
			assert.True(t, e.Cached)
			break
		}
	}
	assert.True(t, cached, "expected at least one cached step event")
}

func TestProcessBuildOutputWithProgress_CachedStepTerminalStatus(t *testing.T) {
	// Verify that a cached step's terminal status is BuildStepCached (not
	// BuildStepComplete). Before the fix, the step-transition logic
	// unconditionally emitted BuildStepComplete for the previous step,
	// overwriting the cached status.
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/3 : FROM node:20-slim\n"},
		buildEvent{Stream: " ---> Using cache\n"},
		buildEvent{Stream: "Step 2/3 : RUN apt-get update\n"},
		buildEvent{Stream: "reading package lists...\n"},
		buildEvent{Stream: "Step 3/3 : COPY . /app\n"},
		buildEvent{Stream: " ---> Using cache\n"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.NoError(t, err)

	events := collector.all()

	// Collect only terminal events per step (last event for each stepID).
	terminal := make(map[string]whail.BuildProgressEvent)
	for _, e := range events {
		terminal[e.StepID] = e
	}

	// step-0 (FROM, cached): terminal status must be BuildStepCached.
	step0 := terminal["step-0"]
	assert.Equal(t, whail.BuildStepCached, step0.Status,
		"cached step-0 terminal status should be BuildStepCached, got %v", step0.Status)
	assert.True(t, step0.Cached, "step-0 Cached field should be true")

	// step-1 (RUN, not cached): terminal status must be BuildStepComplete.
	step1 := terminal["step-1"]
	assert.Equal(t, whail.BuildStepComplete, step1.Status,
		"non-cached step-1 terminal status should be BuildStepComplete, got %v", step1.Status)
	assert.False(t, step1.Cached, "step-1 Cached field should be false")

	// step-2 (COPY, cached, final step): terminal status must be BuildStepCached.
	step2 := terminal["step-2"]
	assert.Equal(t, whail.BuildStepCached, step2.Status,
		"cached step-2 (final) terminal status should be BuildStepCached, got %v", step2.Status)
	assert.True(t, step2.Cached, "step-2 Cached field should be true")
}

func TestProcessBuildOutputWithProgress_Error(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/2 : FROM node:20-slim\n"},
		buildEvent{Stream: " ---> abc123\n"},
		buildEvent{Stream: "Step 2/2 : RUN exit 1\n"},
		buildEvent{Error: "The command '/bin/sh -c exit 1' returned a non-zero code: 1"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "exit 1")

	events := collector.all()

	// Should have an error event
	var errEvent *whail.BuildProgressEvent
	for i, e := range events {
		if e.Status == whail.BuildStepError {
			errEvent = &events[i]
			break
		}
	}
	require.NotNil(t, errEvent, "expected an error step event")
	assert.Contains(t, errEvent.Error, "exit 1")
}

func TestProcessBuildOutputWithProgress_LogLines(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : RUN echo hello && echo world\n"},
		buildEvent{Stream: "hello\n"},
		buildEvent{Stream: "world\n"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.NoError(t, err)

	events := collector.all()

	var logs []string
	for _, e := range events {
		if e.LogLine != "" {
			logs = append(logs, e.LogLine)
		}
	}
	assert.Contains(t, logs, "hello")
	assert.Contains(t, logs, "world")
}

func TestBuildImage_OnProgressThreadedToBuildKit(t *testing.T) {
	fake := whailtest.NewFakeAPIClient()
	engine := clawkerEngine(progressCfg, fake)
	client := &Client{Engine: engine, log: logger.Nop()}

	capture := &whailtest.BuildKitCapture{}
	engine.BuildKitImageBuilder = whailtest.FakeBuildKitBuilder(capture)

	var called bool
	progressFn := func(event whail.BuildProgressEvent) {
		called = true
	}

	ctx := context.Background()
	err := client.BuildImage(ctx, bytes.NewReader(nil), BuildImageOpts{
		Tags:            []string{"test:latest"},
		BuildKitEnabled: true,
		ContextDir:      "/tmp/build",
		SuppressOutput:  true,
		OnProgress:      progressFn,
	})
	require.NoError(t, err)

	// Verify OnProgress was forwarded to BuildKit options
	assert.NotNil(t, capture.Opts.OnProgress, "expected OnProgress to be forwarded to BuildKit")
	_ = called // The fake builder doesn't call OnProgress, so called stays false.
}

// buildEventAux builds a buildEvent carrying an aux ID. Inlines the anonymous
// struct so the call sites stay readable.
func buildEventAux(id string) buildEvent {
	return buildEvent{Aux: &struct {
		ID string `json:"ID"`
	}{ID: id}}
}

func TestProcessBuildOutput_OnCompleteFires(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : FROM alpine\n"},
		buildEventAux("sha256:abc123"),
	)

	var got whail.BuildResult
	var called int
	onComplete := func(r whail.BuildResult) {
		called++
		got = r
	}

	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutput(bytes.NewReader(stream), onComplete)
	require.NoError(t, err)
	assert.Equal(t, 1, called, "OnComplete must fire exactly once on success")
	assert.Equal(t, "sha256:abc123", got.ImageID)
}

func TestProcessBuildOutput_OnCompleteSkippedOnError(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : RUN exit 1\n"},
		buildEventAux("sha256:must-not-be-surfaced"),
		buildEvent{Error: "exit code 1"},
	)

	var called int
	onComplete := func(_ whail.BuildResult) { called++ }

	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutput(bytes.NewReader(stream), onComplete)
	require.Error(t, err)
	assert.Equal(t, 0, called, "OnComplete must NOT fire when build errored")
}

func TestProcessBuildOutput_NilOnComplete(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : FROM alpine\n"},
		buildEventAux("sha256:abc"),
	)
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	require.NoError(t, client.processBuildOutput(bytes.NewReader(stream), nil))
}

func TestProcessBuildOutputQuiet_OnCompleteFires(t *testing.T) {
	stream := buildLegacyStream(buildEventAux("sha256:def456"))

	var got whail.BuildResult
	var called int
	onComplete := func(r whail.BuildResult) {
		called++
		got = r
	}

	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputQuiet(bytes.NewReader(stream), onComplete)
	require.NoError(t, err)
	assert.Equal(t, 1, called)
	assert.Equal(t, "sha256:def456", got.ImageID)
}

func TestProcessBuildOutputQuiet_OnCompleteSkippedOnError(t *testing.T) {
	stream := buildLegacyStream(
		buildEventAux("sha256:must-not-be-surfaced"),
		buildEvent{Error: "boom"},
	)
	var called int
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputQuiet(bytes.NewReader(stream), func(_ whail.BuildResult) { called++ })
	require.Error(t, err)
	assert.Equal(t, 0, called)
}

func TestProcessBuildOutputWithProgress_OnCompleteFires(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : FROM alpine\n"},
		buildEventAux("sha256:ghi789"),
	)

	var got whail.BuildResult
	var called int
	onComplete := func(r whail.BuildResult) {
		called++
		got = r
	}
	collector := &eventCollector{}

	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, onComplete)
	require.NoError(t, err)
	assert.Equal(t, 1, called)
	assert.Equal(t, "sha256:ghi789", got.ImageID)
}

func TestProcessBuildOutputWithProgress_OnCompleteSkippedOnError(t *testing.T) {
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/1 : RUN exit 1\n"},
		buildEventAux("sha256:must-not-be-surfaced"),
		buildEvent{Error: "exit code 1"},
	)
	var called int
	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, func(_ whail.BuildResult) { called++ })
	require.Error(t, err)
	assert.Equal(t, 0, called)
}

func TestProcessBuildOutputWithProgress_MultiStep(t *testing.T) {
	// Verify a multi-step build produces correct step indices and completion
	stream := buildLegacyStream(
		buildEvent{Stream: "Step 1/3 : FROM alpine\n"},
		buildEvent{Stream: " ---> abc123\n"},
		buildEvent{Stream: "Step 2/3 : RUN echo hello\n"},
		buildEvent{Stream: "hello\n"},
		buildEvent{Stream: "Step 3/3 : CMD echo done\n"},
	)

	collector := &eventCollector{}
	client := &Client{Engine: clawkerEngine(progressCfg, whailtest.NewFakeAPIClient()), log: logger.Nop()}
	err := client.processBuildOutputWithProgress(bytes.NewReader(stream), collector.collect, nil)
	require.NoError(t, err)

	events := collector.all()
	require.NotEmpty(t, events)

	// Verify all three step IDs appear
	stepIDs := make(map[string]bool)
	for _, e := range events {
		if e.StepID != "" {
			stepIDs[e.StepID] = true
		}
	}
	assert.True(t, stepIDs["step-0"], "expected step-0")
	assert.True(t, stepIDs["step-1"], "expected step-1")
	assert.True(t, stepIDs["step-2"], "expected step-2")

	// Last event should be complete for the final step
	lastEvent := events[len(events)-1]
	assert.Equal(t, whail.BuildStepComplete, lastEvent.Status)
	assert.Equal(t, "step-2", lastEvent.StepID)
}
