package diagnosticbundle

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/json"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestWriteScopesAndScrubsDiagnosticArchive(t *testing.T) {
	configDir := t.TempDir()
	logPath := filepath.Join(configDir, "logs", "agentteam.log")
	require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0o755))
	require.NoError(t, os.WriteFile(logPath, []byte(strings.Join([]string{
		`time=1 level=INFO msg="bridge" agent_id=agent-a token=daemon-secret`,
		`time=2 level=INFO msg="other" agent_id=agent-a-other token=foreign-secret`,
		`time=3 level=INFO msg="other" agent_id=agent-other token=foreign-secret`,
	}, "\n")+"\n"), 0o600))

	diagnosticsDir := filepath.Join(configDir, "agents", "agent-a", "diagnostics")
	require.NoError(t, os.MkdirAll(diagnosticsDir, 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(diagnosticsDir, "opencode-runtime-debug.jsonl"),
		[]byte(`{"agentId":"agent-a","token":"runtime-secret","event":"busy"}`+"\n"),
		0o600,
	))
	require.NoError(t, os.WriteFile(
		filepath.Join(diagnosticsDir, "opencode-serve.log.1"),
		[]byte("Authorization: Bearer serve-secret\n"),
		0o600,
	))
	require.NoError(t, os.WriteFile(
		filepath.Join(configDir, "config.yaml"),
		[]byte("api_key: config-secret\n"),
		0o600,
	))
	require.NoError(t, os.MkdirAll(filepath.Join(configDir, "agents", "agent-other", "diagnostics"), 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(configDir, "agents", "agent-other", "diagnostics", "opencode-runtime-debug.jsonl"),
		[]byte("foreign-agent-secret\n"),
		0o600,
	))

	var output bytes.Buffer
	require.NoError(t, Write(context.Background(), &output, Paths{
		ConfigDir:     configDir,
		DaemonLogPath: logPath,
	}, Scope{
		WorkspaceID: "wsp_1",
		MachineID:   "mch_1",
		Agents: []Agent{
			{ID: "agent-a", Runtime: "opencode", RuntimeDebugEnabled: true},
		},
	}))

	entries := readArchive(t, output.Bytes())
	require.Contains(t, entries, "manifest.json")
	require.Contains(t, entries, "daemon/agentteam.log")
	require.Contains(t, entries, "agents/agent-a/opencode-runtime-debug.jsonl")
	require.Contains(t, entries, "agents/agent-a/opencode-serve.log.1")
	require.NotContains(t, entries, "config.yaml")
	require.NotContains(t, entries, "agents/agent-other/opencode-runtime-debug.jsonl")

	archiveText := string(output.Bytes())
	require.NotContains(t, archiveText, "daemon-secret")
	require.NotContains(t, archiveText, "runtime-secret")
	require.NotContains(t, archiveText, "serve-secret")
	require.NotContains(t, archiveText, "foreign-secret")
	require.NotContains(t, archiveText, "config-secret")
	require.NotContains(t, archiveText, "foreign-agent-secret")
	require.Contains(t, string(entries["daemon/agentteam.log"]), "agent_id=agent-a token=<redacted>")
	require.NotContains(t, string(entries["daemon/agentteam.log"]), "agent-a-other")
	require.NotContains(t, string(entries["agents/agent-a/opencode-runtime-debug.jsonl"]), "runtime-secret")
	require.NotContains(t, string(entries["agents/agent-a/opencode-serve.log.1"]), "serve-secret")

	var manifest manifest
	require.NoError(t, json.Unmarshal(entries["manifest.json"], &manifest))
	require.Equal(t, "wsp_1", manifest.WorkspaceID)
	require.Equal(t, "mch_1", manifest.MachineID)
	require.Equal(t, []Agent{{ID: "agent-a", Runtime: "opencode", RuntimeDebugEnabled: true}}, manifest.Agents)
	for _, file := range manifest.Files {
		require.NotContains(t, file.Path, configDir)
	}
}

func TestWriteRejectsUnsafeAgentPathsAndSymlinks(t *testing.T) {
	configDir := t.TempDir()
	outside := filepath.Join(t.TempDir(), "outside.log")
	require.NoError(t, os.WriteFile(outside, []byte("outside-secret\n"), 0o600))

	diagnosticsDir := filepath.Join(configDir, "agents", "agent-a", "diagnostics")
	require.NoError(t, os.MkdirAll(diagnosticsDir, 0o755))
	symlink := filepath.Join(diagnosticsDir, "opencode-runtime-debug.jsonl")
	if err := os.Symlink(outside, symlink); err != nil {
		t.Skipf("symlink is unavailable: %v", err)
	}
	outsideAgentDir := filepath.Join(filepath.Dir(outside), "outside-agent")
	require.NoError(t, os.MkdirAll(filepath.Join(outsideAgentDir, "diagnostics"), 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(outsideAgentDir, "diagnostics", "opencode-runtime-debug.jsonl"),
		[]byte("parent-symlink-secret\n"),
		0o600,
	))
	require.NoError(t, os.Symlink(outsideAgentDir, filepath.Join(configDir, "agents", "agent-b")))

	var output bytes.Buffer
	require.NoError(t, Write(context.Background(), &output, Paths{ConfigDir: configDir}, Scope{
		Agents: []Agent{
			{ID: "agent-a", Runtime: "opencode"},
			{ID: "agent-b", Runtime: "opencode"},
			{ID: "../escape", Runtime: "opencode"},
		},
	}))

	entries := readArchive(t, output.Bytes())
	require.NotContains(t, entries, "agents/agent-a/opencode-runtime-debug.jsonl")
	require.NotContains(t, string(output.Bytes()), "outside-secret")
	require.NotContains(t, string(output.Bytes()), "parent-symlink-secret")
	var manifest manifest
	require.NoError(t, json.Unmarshal(entries["manifest.json"], &manifest))
	require.Contains(t, manifest.Omitted, omission{
		Path:   "agent:../escape",
		Reason: "invalid_agent_id",
	})
	require.Contains(t, manifest.Omitted, omission{
		Path:   "agents/agent-a/opencode-runtime-debug.jsonl",
		Reason: "symlink_rejected",
	})
	require.Contains(t, manifest.Omitted, omission{
		Path:   "agents/agent-b/opencode-runtime-debug.jsonl",
		Reason: "outside_agent_root",
	})
}

func TestWriteUsesBoundedTailAndHonorsCancellation(t *testing.T) {
	configDir := t.TempDir()
	diagnosticsDir := filepath.Join(configDir, "agents", "agent-a", "diagnostics")
	require.NoError(t, os.MkdirAll(diagnosticsDir, 0o755))
	path := filepath.Join(diagnosticsDir, "opencode-runtime-debug.jsonl")
	require.NoError(t, os.WriteFile(path, []byte(strings.Repeat("old-line\n", 20)+"latest-line\n"), 0o600))
	scope := Scope{Agents: []Agent{{ID: "agent-a", Runtime: "opencode"}}}

	var output bytes.Buffer
	require.NoError(t, write(context.Background(), &output, Paths{ConfigDir: configDir}, scope, bundleLimits{
		maxFileBytes:   40,
		maxBundleBytes: 40,
		maxAgents:      10,
	}))
	entries := readArchive(t, output.Bytes())
	require.Contains(t, string(entries["agents/agent-a/opencode-runtime-debug.jsonl"]), "latest-line")
	require.NotContains(t, string(entries["agents/agent-a/opencode-runtime-debug.jsonl"]), strings.Repeat("old-line\n", 10))
	var manifest manifest
	require.NoError(t, json.Unmarshal(entries["manifest.json"], &manifest))
	require.True(t, manifest.Files[0].Truncated)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	output.Reset()
	require.ErrorIs(t, Write(ctx, &output, Paths{ConfigDir: configDir}, scope), context.Canceled)
}

func readArchive(t *testing.T, data []byte) map[string][]byte {
	t.Helper()
	reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	require.NoError(t, err)
	entries := make(map[string][]byte, len(reader.File))
	for _, file := range reader.File {
		source, err := file.Open()
		require.NoError(t, err)
		content, err := io.ReadAll(source)
		require.NoError(t, err)
		require.NoError(t, source.Close())
		entries[file.Name] = content
	}
	return entries
}
