import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, patch

import lmpub


class LMPubTests(unittest.TestCase):
    def setUp(self):
        self.tmp = Path(tempfile.mkdtemp())
        self.state = self.tmp / "estado"
        self.state.mkdir()
        lmpub.ROOT = self.tmp
        lmpub.STATE = self.state
        lmpub.LOCK_FILE = self.tmp / ".lmpub.lock"
        (self.state / "APROVACOES_DONO.json").write_text(json.dumps({"aprovacoes": [{"pasta": "p", "veredicto": "APROVADO"}]}))
        (self.state / "APROVACOES_BEFORE_AFTER.json").write_text(json.dumps({"aprovacoes": []}))
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": []}))
        (self.state / "CALENDARIO_PUBLICACAO.json").write_text(json.dumps({"fila": []}))

    def test_antidup(self):
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": [{"pasta_biblioteca": "p", "canal": "instagram", "formato": "reel"}]}))
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")):
            ok, reason = lmpub.validate("p", "instagram", "reel")
        self.assertFalse(ok)
        self.assertIn("anti-duplicação", reason)

    def test_antidup_ignora_entradas_de_teste(self):
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": [
            {"pasta_biblioteca": "p", "canal": "instagram", "formato": "reel", "teste": True}
        ]}))
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")):
            ok, reason = lmpub.validate("p", "instagram", "reel")
        self.assertTrue(ok)
        self.assertEqual(reason, "ok")

    def test_antidup_ignora_falha_terminal_confirmada(self):
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": [{
            "pasta_biblioteca": "p", "canal": "instagram", "formato": "reel",
            "status": "failed"
        }]}))
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")):
            ok, reason = lmpub.validate("p", "instagram", "reel")
        self.assertTrue(ok)
        self.assertEqual(reason, "ok")

    def test_facebook_usa_sdk_titulo_e_repete_erro_de_url_uma_vez(self):
        env = {"COMPOSIO_API_KEY": "key", "FB_PAGE_ID": "page"}
        with patch.object(lmpub, "folder_url", return_value="https://bridge/p/reel.mp4"), \
             patch.object(lmpub, "composio_facebook_execute", side_effect=[
                 RuntimeError("Video could not be read from its URL"), {"successful": True, "data": {"id": "x"}}
             ]) as execute:
            result = lmpub.publish_facebook("p", "legenda integral", "TÍTULO QUEIMADO", env)
        self.assertTrue(result["successful"])
        self.assertEqual(execute.call_count, 2)
        arguments = execute.call_args_list[0].args[1]
        self.assertEqual(arguments["title"], "TÍTULO QUEIMADO")
        self.assertEqual(arguments["description"], "legenda integral")
        self.assertEqual(arguments["file_url"], "https://bridge/p/reel.mp4")

    def test_facebook_usa_conexao_oauth_da_cli(self):
        response = {"successful": True, "data": {"id": "page"}}
        with patch.object(lmpub, "composio_execute", return_value=response) as execute:
            result = lmpub.composio_facebook_execute(
                "FACEBOOK_GET_PAGE_DETAILS", {"page_id": "page"}, {})
        self.assertIs(result, response)
        execute.assert_called_once_with(
            "FACEBOOK_GET_PAGE_DETAILS", {"page_id": "page"})

    def test_bloqueado(self):
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")):
            ok, reason = lmpub.validate("p", "instagram", "reel", {"bloqueado": True})
        self.assertFalse(ok)
        self.assertIn("bloqueado", reason)

    def test_story_capa_recusa_quatro_por_cinco(self):
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")), \
             patch.object(lmpub, "media_url", return_value="https://bridge/story.jpg"), \
             patch.object(lmpub, "remote_image_size", return_value=(1080, 1350)):
            ok, reason = lmpub.validate("p", "instagram", "story_capa", media="capa.jpg")
        self.assertFalse(ok)
        self.assertIn("exige 9:16", reason)

    def test_story_capa_aceita_nove_por_dezasseis(self):
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")), \
             patch.object(lmpub, "media_url", return_value="https://bridge/story.jpg"), \
             patch.object(lmpub, "remote_image_size", return_value=(1080, 1920)):
            ok, reason = lmpub.validate("p", "instagram", "story_capa", media="story_capa.jpg")
        self.assertTrue(ok)
        self.assertEqual(reason, "ok")

    def test_before_after_usa_aprovacao_do_manifesto_separado(self):
        (self.state / "APROVACOES_DONO.json").write_text(json.dumps({"aprovacoes": []}))
        (self.state / "APROVACOES_BEFORE_AFTER.json").write_text(json.dumps({
            "aprovacoes": [{"pasta": "before_after_v7/BA_001", "veredicto": "APROVADO"}]
        }))
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")), \
             patch.object(lmpub, "media_url", return_value="https://bridge/BA_001/post.jpg"), \
             patch.object(lmpub, "remote_image_size", return_value=(1080, 1350)):
            ok, reason = lmpub.validate(
                "before_after_v7/BA_001", "instagram", "post_image",
                {"formato": "before_after"}, "post.jpg")
        self.assertTrue(ok)
        self.assertEqual(reason, "ok")

    def test_before_after_post_e_story_tem_planos_separados(self):
        self.assertEqual(
            lmpub.channels_for({"formato": "before_after"}),
            [("instagram", "post_image", "post.jpg")])
        self.assertEqual(
            lmpub.channels_for({"formato": "before_after_story"}),
            [("instagram", "story_before_after", "story.jpg")])

    def test_before_after_story_recusa_quatro_por_cinco(self):
        (self.state / "APROVACOES_DONO.json").write_text(json.dumps({"aprovacoes": []}))
        (self.state / "APROVACOES_BEFORE_AFTER.json").write_text(json.dumps({
            "aprovacoes": [{"pasta": "before_after_v7/BA_001", "veredicto": "APROVADO"}]
        }))
        with patch.object(lmpub, "remote_head", return_value=(True, "ok")), \
             patch.object(lmpub, "media_url", return_value="https://bridge/BA_001/story.jpg"), \
             patch.object(lmpub, "remote_image_size", return_value=(1080, 1350)):
            ok, reason = lmpub.validate(
                "before_after_v7/BA_001", "instagram", "story_before_after",
                {"formato": "before_after_story"}, "story.jpg")
        self.assertFalse(ok)
        self.assertIn("exige 9:16", reason)

    def test_plano_usa_capa_vertical_para_story(self):
        story = next(item for item in lmpub.channels_for({}) if item[1] == "story_capa")
        self.assertEqual(story[2], "story_capa.jpg")

    def test_story_reel_usa_metadata_story_no_buffer(self):
        with patch.object(lmpub, "buffer_graphql", return_value={
            "createPost": {"post": {"id": "story"}}}) as graphql, \
             patch.object(lmpub, "media_url", return_value="https://bridge/reel.mp4"):
            result = lmpub.publish_buffer(
                "p", "instagram", "story_reel", "reel.mp4", "caption",
                {"BUFFER_ACCESS_TOKEN": "token", "BUFFER_CHANNEL_IG": "channel"})
        self.assertEqual(result["id"], "story")
        mutation = graphql.call_args.args[0]
        self.assertIn("type: story", mutation)
        self.assertIn("video:", mutation)

    def test_reel_usa_metadata_reel_no_buffer(self):
        with patch.object(lmpub, "buffer_graphql", return_value={
            "createPost": {"post": {"id": "reel"}}}) as graphql, \
             patch.object(lmpub, "media_url", return_value="https://bridge/reel.mp4"):
            result = lmpub.publish_buffer(
                "p", "instagram", "reel", "reel.mp4", "caption",
                {"BUFFER_ACCESS_TOKEN": "token", "BUFFER_CHANNEL_IG": "channel"})
        self.assertEqual(result["id"], "reel")
        mutation = graphql.call_args.args[0]
        self.assertIn("type: reel", mutation)
        self.assertIn("shouldShareToFeed: true", mutation)

    def test_publicacao_imagem_feed_vai_por_composio(self):
        calls = [
            {"successful": True, "data": {"id": "ig-user"}},
            {"successful": True, "data": {"id": "container"}},
            {"successful": True, "data": {"id": "published"}},
        ]
        with patch.object(lmpub, "composio_execute", side_effect=calls) as execute, \
             patch.object(lmpub, "media_url", return_value="https://bridge/BA_001/post.jpg"):
            result = lmpub.publish_instagram_image(
                "before_after_v7/BA_001", "post_image", "post.jpg",
                "Benfica 2015/16 #BeforeAndAfter")
        self.assertTrue(result["successful"])
        create_args = execute.call_args_list[1].args[1]
        self.assertEqual(create_args["image_url"], "https://bridge/BA_001/post.jpg")
        self.assertIn("%23BeforeAndAfter", create_args["caption"])
        self.assertNotIn("media_type", create_args)

    def test_publicacao_story_imagem_vai_por_composio(self):
        calls = [
            {"successful": True, "data": {"id": "ig-user"}},
            {"successful": True, "data": {"id": "container"}},
            {"successful": True, "data": {"id": "published"}},
        ]
        with patch.object(lmpub, "composio_execute", side_effect=calls) as execute, \
             patch.object(lmpub, "media_url", return_value="https://bridge/BA_001/story.jpg"):
            lmpub.publish_instagram_image(
                "before_after_v7/BA_001", "story_before_after", "story.jpg", "")
        create_args = execute.call_args_list[1].args[1]
        self.assertEqual(create_args["media_type"], "STORIES")
        self.assertNotIn("caption", create_args)

    def test_registo_encontra_id_aninhado_do_composio(self):
        lmpub.record("p", {"titulo": "P", "clube": "C"}, "facebook", "reel", {
            "successful": True, "data": {"id": "facebook-id"}})
        data = json.loads((self.state / "PUBLICACOES.json").read_text())
        self.assertEqual(data["publicacoes"][-1]["permalink"], "facebook-id")

    def test_canal_falha_nao_trava_os_outros(self):
        slot = {"pasta": "p", "titulo": "P", "pronto": True,
                "_channels": ["instagram/reel", "facebook/reel", "tiktok/video"]}
        with patch.object(lmpub, "validate", return_value=(True, "ok")), \
             patch.object(lmpub, "get_caption", return_value="hello #a #b"), \
             patch.object(lmpub, "publish_instagram_reel", side_effect=RuntimeError("fail")), \
             patch.object(lmpub, "publish_facebook", return_value={"id": "f"}), \
             patch.object(lmpub, "publish_tiktok", return_value={"id": "t"}), \
             patch.object(lmpub, "record", side_effect=[
                 {"status": "legacy_confirmation"}, {"status": "scheduled"}
             ]) as record:
            code = lmpub.execute("p", slot, True)
        self.assertEqual(code, 1)
        self.assertEqual(record.call_count, 2)

    def test_registo_atomico(self):
        lmpub.record("p", {"titulo": "P", "clube": "C"}, "instagram", "reel", {
            "id": "x", "timestamp": "2026-10-07T12:00:00+01:00",
            "permalink": "https://www.instagram.com/reel/x/"
        })
        data = json.loads((self.state / "PUBLICACOES.json").read_text())
        self.assertEqual(data["publicacoes"][-1]["metodo"], "composio-reels")
        self.assertFalse(list(self.state.glob("*.tmp")))

    def test_execute_recusa_slot_nao_pronto_antes_de_validar(self):
        slot = {"pasta": "p", "titulo": "P", "pronto": False}
        with patch.object(lmpub, "validate") as validate:
            code = lmpub.execute("p", slot, True)
        self.assertEqual(code, 1)
        validate.assert_not_called()

    def test_quota_metricool(self):
        month = lmpub.now().strftime("%Y-%m")
        pubs = [{"canal": "tiktok", "publicado_em": f"{month}-01T00:00:00"} for _ in range(20)]
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": pubs}))
        with self.assertRaises(RuntimeError):
            lmpub.publish_tiktok("p", "caption", "reel.mp4", {"METRICOOL_USER_TOKEN": "x"})

    def test_flash_reserva_seis_para_principais(self):
        month = lmpub.now().strftime("%Y-%m")
        pubs = [{"canal": "tiktok", "publicado_em": f"{month}-01T00:00:00"} for _ in range(14)]
        (self.state / "PUBLICACOES.json").write_text(json.dumps({"publicacoes": pubs}))
        (self.state / "CALENDARIO_PUBLICACAO.json").write_text(json.dumps({"fila": [{"pasta": "p", "formato": "flash"}]}))
        with self.assertRaisesRegex(RuntimeError, "reservar principais"):
            lmpub.publish_tiktok("p", "caption", "flash.mp4", {"METRICOOL_USER_TOKEN": "x"})

    def test_tiktok_metricool_recebe_media_como_url_string(self):
        slot = {"pasta": "p", "formato": "reel", "data": lmpub.today().isoformat(), "hora": "21:00"}
        (self.state / "CALENDARIO_PUBLICACAO.json").write_text(json.dumps({"fila": [slot]}))
        call = AsyncMock(return_value={"data": {"id": "tiktok"}})
        with patch.object(lmpub, "metricool_call", call), \
             patch.object(lmpub, "folder_url", return_value="https://bridge/reel.mp4"):
            lmpub.publish_tiktok("p", "caption", "reel.mp4", {}, slot)
        info = json.loads(call.call_args.args[1]["info"])
        self.assertEqual(info["media"], ["https://bridge/reel.mp4"])

    def test_trocar_incompleta_so_pode_descer(self):
        fila = [
            {"pasta": "incompleta", "data": "2026-08-16", "clube": "Sporting CP", "pronto": False},
            {"pasta": "pronta", "data": "2026-09-01", "clube": "Porto", "pronto": True},
        ]
        (self.state / "APROVACOES_DONO.json").write_text(json.dumps({"aprovacoes": [
            {"pasta": "incompleta", "veredicto": "APROVADO"}, {"pasta": "pronta", "veredicto": "APROVADO"}]}))
        (self.state / "CALENDARIO_PUBLICACAO.json").write_text(json.dumps({"fila": fila}))
        args = type("Args", (), {"folder_a": "incompleta", "folder_b": "pronta"})()
        with patch.object(lmpub, "marker_exists", return_value=False):
            self.assertEqual(lmpub.cmd_trocar(args), 0)
        saved = json.loads((self.state / "CALENDARIO_PUBLICACAO.json").read_text())["fila"]
        dates = {x["pasta"]: x["data"] for x in saved}
        self.assertEqual(dates["pronta"], "2026-08-16")
        self.assertEqual(dates["incompleta"], "2026-09-01")

    def test_reordenar_altera_hora(self):
        fila = [{"pasta": "p", "data": "2026-08-12", "hora": "21:00", "clube": "Sporting CP"}]
        (self.state / "CALENDARIO_PUBLICACAO.json").write_text(json.dumps({"fila": fila}))
        args = type("Args", (), {"folder": "p", "date": "2026-08-12", "hora": "19:00"})()
        self.assertEqual(lmpub.cmd_reordenar(args), 0)
        saved = json.loads((self.state / "CALENDARIO_PUBLICACAO.json").read_text())["fila"][0]
        self.assertEqual(saved["hora"], "19:00")

    def test_ordens_por_data_preferem_post_das_dezanove(self):
        fila = [
            {"pasta": "story-a", "data": "2026-08-14", "hora": "10:00", "formato": "before_after_story"},
            {"pasta": "video", "data": "2026-08-14", "hora": "19:00", "formato": "principal"},
            {"pasta": "story-b", "data": "2026-08-14", "hora": "12:00", "formato": "before_after_story"},
        ]
        self.assertEqual(lmpub.slot_by_date(fila, "2026-08-14")["pasta"], "video")

    def test_adjacencia_ignora_stories(self):
        fila = [
            {"pasta": "feed-a", "data": "2026-08-14", "hora": "19:00", "clube": "Porto"},
            {"pasta": "story", "data": "2026-08-15", "hora": "10:00", "clube": "Porto", "formato": "before_after_story"},
            {"pasta": "feed-b", "data": "2026-08-15", "hora": "19:00", "clube": "Benfica"},
        ]
        self.assertIsNone(lmpub.adjacency_error(fila))

    def test_lock(self):
        with lmpub.publisher_lock():
            with self.assertRaises(RuntimeError):
                with lmpub.publisher_lock():
                    pass


if __name__ == "__main__":
    unittest.main()
