-- Portal Pro :: Core/Debug.lua
-- Test harness.
--
-- Classic Era has no unit-test runner, and the alternative to this panel is debugging
-- gold accounting in Orgrimmar at 2am with a real customer waiting. It injects synthetic
-- leads, payments and refunds so the parser and the ledger can be exercised on demand.

local ADDON_NAME, PP = ...
local UI, T, U = PP.UI, PP.Theme, PP.U

-- Named Sandbox, not Debug: PP:Debug() is the logging method on the addon table, and
-- assigning PP.Debug here would silently replace it everywhere.
local Sandbox = PP:NewModule("Sandbox")
PP.Sandbox = Sandbox

local frame

local SAMPLE_NAMES = { "Thraldin", "Mirabelle", "Gorrok", "Sylvenna", "Bramblefoot", "Kaz", "Ordwyn" }
local SAMPLE_CLASSES = { "WARRIOR", "MAGE", "ROGUE", "PRIEST", "HUNTER", "WARLOCK", "DRUID" }
local SAMPLE_MESSAGES = {
	"lf port to sw, will tip",
	"anyone got a portal to ironforge?",
	"need port darnassus plz",
	"wtb portal to org 20g",
	"any mage around for a tele to uc",
	"port to thunder bluff? paying well",
	"lfp sw",
}

local function pick(list)
	return list[math.random(#list)]
end

--------------------------------------------------------------------------------
-- Injectors
--------------------------------------------------------------------------------

function Sandbox:InjectLead(message, name)
	message = message or pick(SAMPLE_MESSAGES)
	name = name or pick(SAMPLE_NAMES) .. math.random(10, 99)

	local score, dest, reasons = PP.Leads:Score(message)
	-- Bypass the per-player cooldown and channel filters: this is a test injection,
	-- not a real chat line.
	PP.Leads:Add({
		name = name,
		class = pick(SAMPLE_CLASSES),
		dest = dest,
		destKey = dest and dest.key or nil,
		message = message,
		channel = "TRADE",
		channelLabel = "Trade - City (test)",
		score = score,
		reasons = reasons,
	})
	PP:Print(("Injected lead from %s (score %d)."):format(name, score))
end

function Sandbox:InjectPayment(name, copper, destKey)
	-- Prefer paying someone already in the queue. Paying a random stranger would never
	-- exercise the "clear the lead once they've paid" path, which is the interesting part.
	if not name then
		local lead = PP.Leads.queue[1]
		if lead then
			name = lead.name
			destKey = destKey or lead.destKey
		end
	end
	name = name or pick(SAMPLE_NAMES) .. math.random(10, 99)
	copper = copper or math.random(5, 30) * 10000
	destKey = destKey or pick(PP.Dest:Available()).key

	PP.Ledger:RecordPayment(name, copper, destKey, "trade", "test injection")
	PP:Print(("Injected payment: %s from %s."):format(U.MoneyPlain(copper), name))

	-- Go through the real event so the queue, stats and any other listener react exactly
	-- as they would to a genuine trade.
	PP:Fire("PAYMENT_RECEIVED", name, copper, destKey)
end

function Sandbox:InjectRefund()
	local payments = PP.Ledger:Query({ kind = "payment" })
	if #payments == 0 then
		PP:Warn("No payments to refund. Inject a payment first.")
		return
	end
	-- Refund the most recent payment that hasn't been fully refunded.
	for i = #payments, 1, -1 do
		local entry = payments[i]
		if PP.Ledger:RefundedAmount(entry.id) < entry.copper then
			PP.Ledger:RefundEntry(entry.id)
			PP:Print(("Refunded %s to %s."):format(U.MoneyPlain(entry.copper), entry.customer))
			return
		end
	end
	PP:Warn("Every payment is already refunded.")
end

function Sandbox:InjectCast()
	local dest = pick(PP.Dest:Available())
	-- force = true: the sandbox should record even inside an instance or with free mode on,
	-- otherwise testing from a dungeon silently does nothing.
	PP.CastTracker:Record(dest, "portal", dest.portal, true)
	PP:Print("Injected portal cast: " .. dest.name)
end

-- Realistic rival adverts and customer bids, for exercising the market feed without
-- waiting for a busy Trade channel.
local SAMPLE_MARKET = {
	{ "WTS portals to any Alliance city 10g, whisper me", 0 },
	{ "Mage ports available 75 silver inv for group", 0 },
	{ "Portals up! 8g each", 0 },
	{ "selling ports 12g", 0 },
	{ "port service 1g50s teleports only", 0 },
	{ "lf port to sw will pay 15g", 5 },
	{ "wtb portal orgrimmar 20g", 6 },
}

--- Push sample lines through the real CHAT_SCANNED pipeline, so this exercises the actual
--- classifier and price parser rather than writing observations directly.
function Sandbox:InjectMarket()
	for _, sample in ipairs(SAMPLE_MARKET) do
		local text, score = sample[1], sample[2]
		local name = pick(SAMPLE_NAMES) .. math.random(10, 99)
		PP:Fire("CHAT_SCANNED", text, name, "TRADE", score, nil)
	end
	local summary = PP.Market:Summary(3600)
	PP:Print(("Injected %d market lines. Asks: %s median from %d, bids: %s median from %d."):format(
		#SAMPLE_MARKET,
		summary.ask.median and U.MoneyPlain(summary.ask.median) or "none", summary.ask.advertisers,
		summary.bid.median and U.MoneyPlain(summary.bid.median) or "none", summary.bid.advertisers))
end

--- Pretend a peer reported a higher version, through the real receive path.
function Sandbox:FakeNewerVersion()
	local parts = {}
	for n in tostring(PP.version):gmatch("%d+") do parts[#parts + 1] = tonumber(n) end

	local newer
	if #parts == 0 then
		-- Unversioned build: still worth simulating, because that's the path a user with a
		-- broken download takes and it needs to produce a notice rather than silence.
		newer = "9.9.9"
		PP:Print("This build reports no version - exercising the unversioned notice.")
	else
		parts[#parts] = parts[#parts] + 1
		newer = table.concat(parts, ".")
	end
	PP.Comms:SimulatePeer(newer, "Testmage")
	PP:Print("Simulated a peer running " .. newer .. ".")
end

--- Numeric-segment comparison must beat string comparison: 1.10.0 is newer than 1.9.0.
function Sandbox:TestVersionCompare()
	local cases = {
		{ "1.2.0", "1.2.0", 0 }, { "1.3.0", "1.2.0", 1 }, { "1.2.0", "1.3.0", -1 },
		{ "1.10.0", "1.9.0", 1 }, { "2.0", "1.99.99", 1 }, { "1.2.1", "1.2", 1 },
		{ "dev", "1.2.0", nil }, { "1.2.0", "dev", nil }, { "1.2.0-beta", "1.2.0", nil },
	}
	local failed = 0
	for _, case in ipairs(cases) do
		local got = PP.Comms:Compare(case[1], case[2])
		if got ~= case[3] then
			failed = failed + 1
			PP:Error(("compare(%s, %s) = %s, expected %s"):format(
				case[1], case[2], tostring(got), tostring(case[3])))
		end
	end
	if failed == 0 then
		PP:Print(("Version comparison: all %d cases passed."):format(#cases))
	end
end

--- Verify that the money arbiter routes a delta to the right claimant.
function Sandbox:TestMoneyRouting()
	local partner = "TestCustomer" .. math.random(10, 99)
	PP.MoneyWatch:Expect("trade", partner, 5, function(delta, who, source)
		PP:Print(("MoneyWatch routed %s from %s via %s."):format(U.MoneyPlain(delta), who, source))
	end)
	PP:Print("Claim registered. Now change your gold (sell something) within 5s to confirm routing.")
end

--------------------------------------------------------------------------------
-- Panel
--------------------------------------------------------------------------------

local function build()
	if frame then return frame end

	frame = UI:Panel(UIParent, "overlay")
	frame:SetSize(360, 430)
	frame:SetPoint("CENTER", 240, 0)
	frame:SetFrameStrata("DIALOG")
	frame:Hide()
	UI:Border(frame, "warning")
	UI:Shadow(frame, 5)
	UI:MakeDraggable(frame)

	local title = UI:Text(frame, "Portal Pro - Sandbox", T.fontSize.md, "", "warning")
	title:SetPoint("TOPLEFT", T.space.md, -T.space.md)

	local sub = UI:Text(frame, "Synthetic events. Nothing here touches other players.",
		T.fontSize.sm, "", "textMuted")
	sub:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)

	local close = UI:Button(frame, "X", function() frame:Hide() end, "ghost", 26, 26)
	close:SetPoint("TOPRIGHT", -T.space.sm, -T.space.sm)

	local y = -60
	local function addPair(labelA, handlerA, labelB, handlerB)
		local a = UI:Button(frame, labelA, handlerA, "secondary", 155, 24)
		a:SetPoint("TOPLEFT", T.space.md, y)
		if labelB then
			local b = UI:Button(frame, labelB, handlerB, "secondary", 155, 24)
			b:SetPoint("TOPLEFT", T.space.md + 163, y)
		end
		y = y - 30
	end

	addPair("Inject random lead", function() Sandbox:InjectLead() end,
	        "Inject payment", function() Sandbox:InjectPayment() end)
	addPair("Inject refund", function() Sandbox:InjectRefund() end,
	        "Inject portal cast", function() Sandbox:InjectCast() end)
	addPair("Test money routing", function() Sandbox:TestMoneyRouting() end,
	        "Fire 5 leads", function()
		for i = 1, 5 do Sandbox:InjectLead() end
	end)
	addPair("Inject market data", function() Sandbox:InjectMarket() end,
	        "Clear market data", function() PP.Market:Clear() end)
	addPair("Fake newer version", function() Sandbox:FakeNewerVersion() end,
	        "Test version compare", function() Sandbox:TestVersionCompare() end)

	y = y - T.space.md

	-- Score tester
	local scoreLabel = UI:Label(frame, "Score a message")
	scoreLabel:SetPoint("TOPLEFT", T.space.md, y)
	y = y - 18

	local input = UI:Input(frame, "lf port to sw", "", nil)
	input:SetPoint("TOPLEFT", T.space.md, y)
	input:SetWidth(230)

	local result = UI:Text(frame, "", T.fontSize.sm, "", "textSecondary")
	result:SetWordWrap(true)
	result:SetJustifyH("LEFT")

	local runBtn = UI:Button(frame, "Score", function()
		local text = input:GetValue()
		if U.Trim(text) == "" then return end
		local score, dest, reasons = PP.Leads:Score(text)
		local lines = {
			("|cff%s%d|r - %s%s"):format(
				score >= 3 and T.hex.positive or T.hex.negative,
				score,
				score >= 3 and "would create a lead" or "ignored",
				dest and (" · " .. dest.name) or ""),
		}
		for _, reason in ipairs(reasons) do lines[#lines + 1] = "  " .. reason end
		result:SetText(table.concat(lines, "\n"))
	end, "primary", 70, T.size.inputHeight)
	runBtn:SetPoint("LEFT", input, "RIGHT", T.space.sm, 0)

	y = y - 34
	result:SetPoint("TOPLEFT", T.space.md, y)
	result:SetPoint("RIGHT", frame, "RIGHT", -T.space.md, 0)

	-- Live counters
	local counters = UI:Text(frame, "", T.fontSize.sm, "", "textMuted")
	counters:SetPoint("BOTTOMLEFT", T.space.md, T.space.md)
	counters:SetPoint("RIGHT", frame, "RIGHT", -T.space.md, 0)
	counters:SetJustifyH("LEFT")
	counters:SetWordWrap(true)
	frame.counters = counters

	frame:SetScript("OnShow", function(self)
		self.ticker = C_Timer.NewTicker(1, function()
			local c = PP.global.counters
			self.counters:SetText((
				"leads %d · whispers %d · invites %d · casts %d · broadcasts %d\n" ..
				"ledger entries %d · customers %d"
			):format(c.leads or 0, c.whispers or 0, c.invites or 0, c.casts or 0, c.broadcasts or 0,
				#PP.Ledger:All(), U.Count(PP.global.customers)))
		end)
	end)
	frame:SetScript("OnHide", function(self)
		if self.ticker then
			self.ticker:Cancel()
			self.ticker = nil
		end
	end)

	return frame
end

function Sandbox:Toggle()
	build()
	if frame:IsShown() then frame:Hide() else frame:Show() end
end
