# -*- coding: utf-8 -*-
# Copyright (c) 2020, JINGROW
# See license.txt


import json
import unittest
from contextlib import contextmanager
from typing import Callable, Literal
from unittest.mock import Mock, patch

import jingrow
import responses
from jingrow.model.naming import make_autoname

from jcloud.agent import Agent
from jcloud.jcloud.pagetype.agent_job.agent_job import AgentJob, lock_pg_updated_by_job
from jcloud.jcloud.pagetype.site.test_site import create_test_bench, create_test_site
from jcloud.jcloud.pagetype.team.test_team import create_test_jcloud_admin_team
from jcloud.utils.test import foreground_enqueue, foreground_enqueue_pg


def fn_appender(before_insert: Callable, prepare_agent_responses: Callable):
	def new_before_insert(self):
		before_insert(self)
		prepare_agent_responses(self)

	return new_before_insert


def before_insert(self):
	return None


def fake_agent_job_req(
	job_type: str,
	status: Literal["Success", "Pending", "Running", "Failure"],
	data: dict,
	steps: list[dict],
) -> Callable:
	data = data or {}
	steps = steps or []

	def prepare_agent_responses(self):
		"""
		Fake successful delivery with fake job id

		Also return fake result on polling
		steps: list of {"name": "Step name", "status": "status"} dictionaries
		"""
		nonlocal status
		nonlocal job_type
		if self.job_type != job_type:  # only fake the job we want to fake
			return
		job_id = int(make_autoname(".#"))
		if steps:
			needed_steps = jingrow.get_all(
				"Agent Job Type Step", {"parent": job_type}, pluck="step_name"
			)
			for step in needed_steps:
				if not any(step == s["name"] for s in steps):
					steps.append(
						{
							"name": step,
							"status": "Success",
							"data": {},
						}
					)

		for step in steps:
			step["start"] = "2023-08-20 18:24:28.024885"
			step["data"] = {}
			if step["status"] in ["Success", "Failure"]:
				step["duration"] = "00:00:13.464445"
				step["end"] = "2023-08-20 18:24:41.489330"
			if step["status"] in ["Success", "Failure", "Running"]:
				step["start"] = "2023-08-20 18:24:28.024885"
				step["end"] = None
				step["duration"] = None

		# TODO: auto add response corresponding to request type #
		responses.post(
			f"https://{self.server}:443/agent/{self.request_path}",
			json={"job": job_id},
			status=200,
		)
		responses.delete(
			f"https://{self.server}:443/agent/{self.request_path}",
			json={"job": job_id},
			status=200,
		)
		# TODO: make the next url regex for multiple job ids #
		responses.add(
			responses.GET,
			f"https://{self.server}:443/agent/jobs/{str(job_id)}",
			# TODO:  populate steps with data from agent job type #
			json={
				"data": data,
				# TODO: uncomment lines as needed and make new parameters #
				"duration": "00:00:13.496281",
				"end": "2023-08-20 18:24:41.506067",
				# "enqueue": "2023-08-20 18:24:27.907340",
				"id": job_id,
				# "name": "Install App on Site",
				"start": "2023-08-20 18:24:28.009786",
				"status": status,
				"steps": steps
				or [
					{
						"data": {
							# "command": "docker exec -w /home/jingrow/jingrow-bench	bench-0001-000025-f1 bench --site fdesk-old.local.jingrow.dev install-app helpdesk",
							# "directory": "/home/jingrow/benches/bench-0001-000025-f1",
							# "duration": "0:00:13.447104",
							# "end": "2023-08-20 18:24:41.482031",
							# "output": "Installing helpdesk...\nUpdating PageTypes for helpdesk\t	 : [========================================] 100%\nUpdating Dashboard for helpdesk",
							# "start": "2023-08-20 18:24:28.034927",
						},
						"duration": "00:00:13.464445",
						"end": "2023-08-20 18:24:41.489330",
						# "id": 1350,
						"name": job_type,
						"start": "2023-08-20 18:24:28.024885",
						"status": status,
					}
				],
			},
			status=200,
		)

	global before_insert
	before_insert = fn_appender(before_insert, prepare_agent_responses)
	return before_insert


@contextmanager
def fake_agent_job(
	job_type: str,
	status: Literal["Success", "Pending", "Running", "Failure"] = "Success",
	data: dict = None,
	steps: list[dict] = None,
):
	"""Fakes agent job request and response. Also polls the job.

	HEADS UP: Don't use this when you're mocking enqueue_http_request in your test context
	"""
	with responses.mock, patch.object(
		AgentJob,
		"before_insert",
		fake_agent_job_req(job_type, status, data, steps),
		create=True,
	), patch(
		"jcloud.jcloud.pagetype.agent_job.agent_job.jingrow.enqueue_pg",
		new=foreground_enqueue_pg,
	), patch(
		"jcloud.jcloud.pagetype.agent_job.agent_job.jingrow.enqueue",
		new=foreground_enqueue,
	), patch(
		"jcloud.jcloud.pagetype.agent_job.agent_job.jingrow.db.commit", new=Mock()
	), patch(
		"jcloud.jcloud.pagetype.agent_job.agent_job.jingrow.db.rollback", new=Mock()
	):
		jingrow.local.role_permissions = (
			{}
		)  # due to bug in FF related to only_if_creator pageperm
		yield
		global before_insert
		before_insert = lambda self: None  # noqa


@patch.object(AgentJob, "enqueue_http_request", new=Mock())
class TestAgentJob(unittest.TestCase):
	def setUp(self):
		self.team = create_test_jcloud_admin_team()
		self.team.allocate_credit_amount(1000, source="Prepaid Credits", remark="Test")
		self.team.payment_mode = "Prepaid Credits"
		self.team.save()

	def tearDown(self):
		jingrow.db.rollback()
		jingrow.set_user("Administrator")

	@patch.object(Agent, "reload_nginx")
	def test_suspend_sites_issues_reload_in_bulk(self, mock_reload_nginx):
		from .agent_job import suspend_sites

		bench1 = create_test_bench()
		bench2 = create_test_bench()
		bench3 = create_test_bench()

		jingrow.set_user(self.team.user)
		site1 = create_test_site(bench=bench1)
		site2 = create_test_site(bench=bench2)
		create_test_site(bench=bench3)  # control; no suspend

		site1.db_set("current_database_usage", 101)
		site2.db_set("current_disk_usage", 101)
		jingrow.db.set_single_value("Jcloud Settings", "enforce_storage_limits", True)
		suspend_sites()
		suspend_jobs = jingrow.get_all(
			"Agent Job", {"job_type": "Update Site Status"}, ["request_data"]
		)
		for job in suspend_jobs:
			self.assertTrue(json.loads(job.request_data).get("skip_reload"))

		self.assertEqual(len(suspend_jobs), 2)
		self.assertEqual(
			mock_reload_nginx.call_count,
			jingrow.db.count("Proxy Server", {"status": "Active"}),
		)

	def test_lock_pg_updated_by_job_respects_hierarchy(self):
		"""
		Site > Bench > Server
		"""
		site = create_test_site()  # creates job
		site.update_site_config({"maintenance_mode": "1"})
		job = jingrow.get_last_pg("Agent Job", {"job_type": "Update Site Configuration"})
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertIsNone(pg_name)
		job = jingrow.get_last_pg("Agent Job", {"job_type": "New Site"})
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertEqual(site.name, pg_name)
		job.db_set("site", None)
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertEqual(site.bench, pg_name)
		job.db_set("bench", None)
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertEqual(site.server, pg_name)
		job.db_set("server", None)  # will realistically never happen
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertIsNone(pg_name)

	@patch("jcloud.jcloud.pagetype.site.site.create_dns_record", new=Mock())
	@patch("jcloud.jcloud.pagetype.site.site._change_dns_record", new=Mock())
	def test_lock_pg_updated_by_job_locks_on_site_rename(self):
		site = create_test_site()
		site.subdomain = "renamed-domain"
		site.save()
		job = jingrow.get_last_pg("Agent Job", {"job_type": "Rename Site"})
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertEqual(site.name, pg_name)
		job = jingrow.get_last_pg("Agent Job", {"job_type": "Rename Site on Upstream"})
		pg_name = lock_pg_updated_by_job(job.name)
		self.assertEqual(site.name, pg_name)

	def test_no_duplicate_undelivered_job(self):
		site = create_test_site()
		site.update_site_config({"maintenance_mode": "1"})
		job = jingrow.get_last_pg("Agent Job", {"job_type": "Update Site Configuration"})

		jingrow.db.set_single_value("Jcloud Settings", "disable_agent_job_deduplication", False)

		# create a new job with same type and site
		job_name = site.update_site_config({"host_name": f"https://{site.host_name}"})

		self.assertEqual(job_name.name, job.name)

	def test_get_similar_in_execution_job(self):
		site = create_test_site()
		site.update_site_config({"maintenance_mode": "1"})
		job = jingrow.get_last_pg("Agent Job", {"job_type": "Update Site Configuration"})

		jingrow.db.set_single_value("Jcloud Settings", "disable_agent_job_deduplication", False)

		# check if similar job exists
		agent = Agent(site.server)
		in_execution_job = agent.get_similar_in_execution_job(
			job_type="Update Site Configuration",
			path=f"benches/{site.bench}/sites/{site.name}/config",
			bench=site.bench,
			site=site.name,
		)

		self.assertEqual(in_execution_job.name, job.name)

		jingrow.db.set_single_value("Jcloud Settings", "disable_agent_job_deduplication", True)
