# noise_module.py
# Versão: 7.2 (Build com Correção de Fonte)
# Data: 01/09/2025 21:46 (São Paulo, Brasil)
# Descrição: Módulo de Análise de Ruído.
#            - CORREÇÃO: Removida a tentativa de obter a fonte do ttk.Treeview que causava um erro TclError.
#            - Edição in-line do nome do teste no histórico com duplo-clique.

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import ctypes, os, struct, threading, time, json
from datetime import datetime, timedelta
from decimal import Decimal, getcontext

try:
    import matplotlib
    matplotlib.use("TkAgg")
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    LIBS_DISPONIVEIS = True
except ImportError:
    LIBS_DISPONIVEIS = False

try:
    from .noise_database import NoiseDatabase
    DATABASE_AVAILABLE = True
except ImportError:
    try:
        from noise_database import NoiseDatabase
        DATABASE_AVAILABLE = True
    except ImportError:
        DATABASE_AVAILABLE = False
        print("⚠️ Módulo de banco de dados não disponível - histórico desabilitado")

try:
    from .state_persistence import StatePersistence
    STATE_PERSISTENCE_AVAILABLE = True
except ImportError:
    try:
        from state_persistence import StatePersistence
        STATE_PERSISTENCE_AVAILABLE = True
    except ImportError:
        STATE_PERSISTENCE_AVAILABLE = False
        print("⚠️ Sistema de persistência de estado não disponível")

try:
    from .i18n import get_translator, t
    TRANSLATOR_AVAILABLE = True
except ImportError:
    try:
        from i18n import get_translator, t
        TRANSLATOR_AVAILABLE = True
    except ImportError:
        TRANSLATOR_AVAILABLE = False
        get_translator = None
        t = None
        print("⚠️ Sistema de traduções indisponível para NoiseModule")

# --- CONFIGURAÇÕES E CONSTANTES DO MÓULO ---
getcontext().prec = 10
DLL_NAME = "UHFRFID.dll"
# COM_PORT = 4 ### ALTERADO: Removido
BAUD_RATE = 115200
FIXED_FREQ_MHZ = 915.0
COMMAND_CODE_SET_FREQ = 0x14
RFID_CMD_GET_RSSIVALU = 0x64
RFID_CMD_GET_TEMPERATURE = 0x34
TEMPERATURE_LIMIT = 50.0

try:
    dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), DLL_NAME)
    rfid_sdk = ctypes.CDLL(dll_path)
    rfid_sdk.UHF_RFID_Open.argtypes = [ctypes.c_ubyte, ctypes.c_int]
    rfid_sdk.UHF_RFID_Open.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Close.argtypes = [ctypes.c_ubyte]
    rfid_sdk.UHF_RFID_Close.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Set.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint)]
    rfid_sdk.UHF_RFID_Set.restype = ctypes.c_int
except OSError:
    rfid_sdk = None

class NoiseModule(ttk.Frame):
    ### ALTERADO: Adicionado com_port=4 ###
    def __init__(self, master=None, is_licensed=False, app_shell=None, demo_mode=False, com_port=4):
        super().__init__(master)
        self.master = master
        self.is_licensed = is_licensed
        self.app_shell = app_shell
        self.demo_mode = demo_mode
        self.com_port = com_port ### NOVO: Armazena a porta COM ###
        
        # NOVO: Define o nome do módulo para identificação
        self.module_name = "Noise Check"
        
        # NOVO: Dicionário para persistir cores dos testes
        self.test_colors = {}
        self._load_test_colors()

        # CORREÇÃO: Sistema de Limites de Licença
        self.license_limits = {'min_freq': 800, 'max_freq': 1000, 'min_power': 5, 'max_power': 25, 'is_licensed': False}
        
        # CORREÇÃO: Atualiza limites da licença se app_shell estiver disponível
        if app_shell and hasattr(app_shell, 'license_limits'):
            self.license_limits = app_shell.license_limits
            print(f"🔍 NoiseModule: License_limits do app_shell: {self.license_limits}")
        elif app_shell and hasattr(app_shell, '_calculate_license_limits'):
            # Tenta obter limites da licença ativa
            try:
                valid_lics = ["NoiseCheck", "FastChecker"]
                license_limits = app_shell._calculate_license_limits(valid_lics)
                if license_limits.get('is_licensed', False):
                    self.license_limits = license_limits
                    print(f"✅ NoiseCheck: Limites da licença aplicados - Freq: {license_limits.get('min_freq')}-{license_limits.get('max_freq')}MHz, Power: {license_limits.get('min_power')}-{license_limits.get('max_power')}dBm")
                    print(f"✅ NoiseCheck: Licença ativa: '{license_limits.get('license_name', 'N/A')}'")
                else:
                    print("⚠️ NoiseCheck: Usando limites padrão (sem licença ativa)")
                    print(f"🔍 NoiseCheck: License_limits recebidos: {license_limits}")
            except Exception as e:
                print(f"⚠️ NoiseCheck: Erro ao obter limites da licença: {e}")
                print("⚠️ NoiseCheck: Usando limites padrão")
        
        # CORREÇÃO: Atualiza o status da licença baseado nos limites obtidos
        if self.license_limits.get('is_licensed', False):
            self.is_licensed = True
            print("✅ NoiseModule: Status da licença definido como True baseado nos limites")
        else:
            self.is_licensed = False
            print("⚠️ NoiseModule: Status da licença definido como False (sem licença válida)")

        self.test_name_var = tk.StringVar(value="Noise")
        self.scan_time_var = tk.StringVar(value="60")
        self.run_mode_var = tk.StringVar(value="Single")
        # CORREÇÃO: Status inicial contextual baseado na licença
        is_licensed = self.is_licensed
        initial_status = "Modo browser - funcionalidade limitada" if not is_licensed else "Pronto para iniciar teste de ruído"
        self.status_var = tk.StringVar(value=initial_status)

        self.test_is_running = False
        self.stop_requested = False
        self.worker_thread = None

        self.live_data, self.imported_data = {}, {}
        self.multiple_tests = {}
        self.current_test_name = ""
        self.annot = None
        self.test_start_time = None
        self.current_sort_column = None
        self.current_sort_reverse = False  # NOVO: Armazena o horário de início do teste
        self._last_plot_update = 0.0
        self._plot_update_interval = 0.3  # segundos entre atualizações completas do gráfico
        
        # Sistema de traduções
        self.translator = get_translator() if TRANSLATOR_AVAILABLE and callable(get_translator) else None
        self._widget_refs = {}
        self._translation_cache = {}
        self._last_status_message = None
        self._last_status_time = 0.0
        self._status_update_interval = 0.4  # segundos entre atualizações frequentes
        if self.translator:
            try:
                self.translator.add_language_change_listener(self._on_language_changed)
            except Exception as e:
                print(f"⚠️ NoiseModule: Não foi possível registrar listener de idioma: {e}")
        
        # Variáveis de zoom
        self.zoom_factor = 1.0
        self.original_xlim = None
        self.original_ylim = None

        self.database = None
        self.test_history = []
        if DATABASE_AVAILABLE:
            try:
                self.database = NoiseDatabase()
                # NOVO: Executa migração de dados antigos
                self.database.migrate_old_data()
                self.test_history = self.database.get_test_history()
                print(f"📊 Carregados {len(self.test_history)} testes de ruído do banco de dados")
            except Exception as e:
                print(f"⚠️ Erro ao carregar banco de dados: {e}")
                self.database = None
        
        # NOVO: Sistema de Persistência de Estado
        self.state_persistence = None
        if STATE_PERSISTENCE_AVAILABLE:
            try:
                # Cria sistema de persistência específico para o noise module
                state_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "noise_state.json")
                self.state_persistence = StatePersistence(state_file)
                print("✅ Sistema de persistência de estado inicializado para Noise Module")
            except Exception as e:
                print(f"⚠️ Erro ao inicializar persistência de estado: {e}")
                self.state_persistence = None
        
        self.create_widgets()
        self.update_ui_state(False)
        self._apply_translations()
        
        # NOVO: Carrega estado anterior
        self._load_previous_state()
        
        self.bind("<Destroy>", self.on_destroy)

        if not rfid_sdk or not LIBS_DISPONIVEIS:
            self.show_dependency_error()

    def on_destroy(self, event=None):
        # NOVO: Salva estado antes de destruir
        self._save_current_state()
        
        if self.translator:
            try:
                self.translator.remove_language_change_listener(self._on_language_changed)
            except Exception as e:
                print(f"⚠️ NoiseModule: Não foi possível remover listener de idioma: {e}")
        
        if self.test_is_running:
            self.stop_requested = True
            self.test_is_running = False
            if self.worker_thread and self.worker_thread.is_alive():
                self.worker_thread.join(timeout=0.5)
        if rfid_sdk:
            ### ALTERADO: usa self.com_port ###
            try: rfid_sdk.UHF_RFID_Close(self.com_port)
            except Exception as e: print(f"Erro ao fechar COM no módulo de ruído: {e}")

    def show_dependency_error(self):
        for widget in self.winfo_children(): widget.destroy()
        error_msg = ""
        if not LIBS_DISPONIVEIS: error_msg += "Biblioteca 'matplotlib' não encontrada.\nInstale com: pip install matplotlib\n\n"
        if not rfid_sdk: error_msg += f"Não foi possível carregar a DLL '{DLL_NAME}'.\nEste módulo não funcionará."
        ttk.Label(self, text=error_msg, justify="center", font=("Segoe UI", 12, "bold"), foreground="red").pack(expand=True, padx=20, pady=20)

    def _load_previous_state(self):
        """Carrega o estado anterior da interface"""
        if not self.state_persistence:
            return
        
        try:
            state = self.state_persistence.load_state()
            if state and "interface_config" in state:
                config = state["interface_config"]
                
                # Carrega valores da interface
                if "test_name" in config:
                    self.test_name_var.set(config["test_name"])
                if "scan_time" in config:
                    self.scan_time_var.set(config["scan_time"])
                if "run_mode" in config:
                    self.run_mode_var.set(config["run_mode"])
                
                print("✅ Noise Module: Estado anterior carregado com sucesso")
        except Exception as e:
            print(f"⚠️ Erro ao carregar estado anterior: {e}")

    def _save_current_state(self):
        """Salva o estado atual da interface"""
        if not self.state_persistence:
            return
        
        try:
            state = {
                "interface_config": {
                    "test_name": self.test_name_var.get(),
                    "scan_time": self.scan_time_var.get(),
                    "run_mode": self.run_mode_var.get()
                },
                "last_session": {
                    "timestamp": datetime.now().isoformat(),
                    "module": "Noise Check"
                }
            }
            
            success = self.state_persistence.save_state(state)
            if success:
                print("💾 Noise Module: Estado salvo com sucesso")
        except Exception as e:
            print(f"⚠️ Erro ao salvar estado: {e}")

    def _schedule_auto_save(self):
        """Agenda salvamento automático após mudanças"""
        if hasattr(self, '_save_timer'):
            self.after_cancel(self._save_timer)
        
        self._save_timer = self.after(5000, self._auto_save_state)  # 5 segundos

    def _auto_save_state(self):
        """Executa salvamento automático"""
        try:
            if hasattr(self, 'state_persistence') and self.state_persistence:
                self._save_current_state()
        except Exception as e:
            print(f"⚠️ Erro no auto-save: {e}")

    def _setup_auto_save_callbacks(self):
        """Configura callbacks para salvamento automático quando valores mudam"""
        if not self.state_persistence:
            return
        
        try:
            # Callback para mudanças nas variáveis
            def on_variable_change(*args):
                self._schedule_auto_save()
            
            # Adiciona callbacks às variáveis StringVar
            self.test_name_var.trace('w', on_variable_change)
            self.scan_time_var.trace('w', on_variable_change)
            self.run_mode_var.trace('w', on_variable_change)
            
            print("✅ Callbacks de auto-save configurados para Noise Module")
        except Exception as e:
            print(f"⚠️ Erro ao configurar callbacks de auto-save: {e}")

    def create_widgets(self):
        # Layout principal: barra lateral + área principal
        main_container = ttk.Frame(self)
        main_container.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Configuração do grid
        main_container.columnconfigure(1, weight=1)  # Área principal ocupa espaço restante
        main_container.rowconfigure(0, weight=1)
        
        # BARRA LATERAL (esquerda)
        self.sidebar = ttk.Frame(main_container, width=300)
        self.sidebar.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
        self.sidebar.grid_propagate(False)  # Mantém largura fixa
        
        # ÁREA PRINCIPAL (direita)
        self.main_area = ttk.Frame(main_container)
        self.main_area.grid(row=0, column=1, sticky="nsew")
        self.main_area.columnconfigure(0, weight=1)
        self.main_area.rowconfigure(0, weight=1)
        
        # Construir componentes da barra lateral
        self._build_sidebar()
        
        # Construir área principal
        self._build_main_area()
        
        # Mensagem de modo browser (se necessário)
        self._setup_browser_mode_message()
        
        # NOVO: Adiciona callbacks de salvamento automático
        self._setup_auto_save_callbacks()

    def _t(self, key, default=None, **kwargs):
        """Retorna a tradução para a chave informada."""
        if self.translator:
            try:
                current_lang = getattr(self.translator, 'get_language', lambda: None)()
                cache_key = (current_lang, key)
                if cache_key in self._translation_cache:
                    text = self._translation_cache[cache_key]
                else:
                    text = self.translator.translate(key, default if default is not None else key)
                    self._translation_cache[cache_key] = text
            except Exception:
                text = default if default is not None else key
        else:
            text = default if default is not None else key
        
        if kwargs:
            try:
                return text.format(**kwargs)
            except Exception as e:
                print(f"⚠️ NoiseModule: Erro ao formatar texto '{key}': {e}")
                return text
        return text
    
    def _set_status(self, key, default=None, *, force=False, **kwargs):
        """Atualiza o texto de status traduzido."""
        if hasattr(self, 'status_var'):
            message = self._t(key, default, **kwargs)
            now = time.time()
            should_update = force or (message != self._last_status_message) or ((now - self._last_status_time) >= self._status_update_interval)
            if should_update:
                self.status_var.set(message)
                self._last_status_message = message
                self._last_status_time = now

    def _on_language_changed(self, *_):
        """Callback disparado quando o idioma é alterado."""
        self._translation_cache.clear()
        self._last_status_message = None
        self._last_status_time = 0.0
        self.after_idle(self._apply_translations)

    def _apply_translations(self):
        """Aplica traduções aos elementos da interface."""
        try:
            # Frames principais
            if hasattr(self, 'config_frame'):
                self.config_frame.configure(text=self._t('noise.test_config', 'Configuração do Teste'))
            if hasattr(self, 'controls_frame'):
                self.controls_frame.configure(text=self._t('noise.controls', 'Controles'))
            if hasattr(self, 'history_frame'):
                self.history_frame.configure(text=self._t('noise.test_history', 'Histórico de Testes'))
            if hasattr(self, 'plot_title_label'):
                self.plot_title_label.configure(text=self._t('noise.monitor_title', 'Monitor de Ruído'))

            # Labels de configuração
            if hasattr(self, 'test_name_label'):
                self.test_name_label.configure(text=self._t('noise.name', 'Nome:'))
            if hasattr(self, 'scan_time_label'):
                self.scan_time_label.configure(text=self._t('noise.time_s', 'Tempo (s):'))
            if hasattr(self, 'mode_label'):
                self.mode_label.configure(text=self._t('noise.mode', 'Modo:'))
            if hasattr(self, 'radio_single'):
                self.radio_single.configure(text=self._t('noise.single', 'Único'))
            if hasattr(self, 'radio_continuous'):
                self.radio_continuous.configure(text=self._t('noise.continuous', 'Contínuo'))

            # Botões da barra lateral
            if hasattr(self, 'btn_start'):
                self.btn_start.configure(text=self._t('noise.test', 'Testar'))
            if hasattr(self, 'btn_stop'):
                self.btn_stop.configure(text=self._t('noise.stop', 'Parar'))
            if hasattr(self, 'btn_clear_plot'):
                self.btn_clear_plot.configure(text=self._t('noise.clear_plot', 'Limpar Gráfico'))
            if hasattr(self, 'btn_save_selected'):
                self.btn_save_selected.configure(text=self._t('noise.save_selected', 'Salvar Selecionados'))
            if hasattr(self, 'btn_import_tests'):
                self.btn_import_tests.configure(text=self._t('noise.import_tests', 'Importar Testes'))
            if hasattr(self, 'btn_generate_report'):
                self.btn_generate_report.configure(text=self._t('noise.report_pdf', 'Relatório Selecionados (PDF)'))

            # Histórico - cabeçalhos
            if hasattr(self, 'history_stats_label'):
                self.update_history_stats()
            if hasattr(self, 'history_tree'):
                self._update_history_headings()
            if hasattr(self, 'select_all_button'):
                self.select_all_button.configure(text=self._t('noise.select_all', 'Selecionar Todos'))
            if hasattr(self, 'deselect_all_button'):
                self.deselect_all_button.configure(text=self._t('noise.deselect_all', 'Deselecionar Todos'))
            if hasattr(self, 'delete_selected_button'):
                self.delete_selected_button.configure(text=self._t('noise.delete_selected', 'Excluir Selecionados...'))

            # Mensagem de modo browser
            if hasattr(self, 'browser_mode_label'):
                self.browser_mode_label.configure(text=self._t('noise.browser_mode', ' modo browser'))

            # Eixos do gráfico
            if hasattr(self, 'ax'):
                self.ax.set_xlabel(self._t('noise.x_axis_label', 'Tempo (s)'))
                self.ax.set_ylabel(self._t('noise.y_axis_label', 'Intensidade (dBm)'))

            # Status inicial
            if hasattr(self, 'status_var') and not self.status_var.get():
                key = 'noise.status_ready' if self.is_licensed else 'noise.status_browser_mode'
                default = 'Pronto para iniciar teste de ruído' if self.is_licensed else 'Modo browser - funcionalidade limitada'
                self._set_status(key, default)

        except Exception as e:
            print(f"⚠️ NoiseModule: Erro ao aplicar traduções: {e}")

    def _update_history_headings(self):
        """Atualiza os títulos das colunas do histórico conforme o idioma."""
        try:
            headings = {
                "Plot": self._t('noise.plot', 'Plot'),
                "ID": self._t('noise.id', 'ID'),
                "Nome": self._t('noise.test_name', 'Nome do Teste'),
                "Duração": self._t('noise.duration', 'Duração (s)'),
                "Ruído Médio": self._t('noise.avg_noise', 'Ruído Médio (dBm)'),
                "Ruído Mínimo": self._t('noise.min_noise', 'Ruído Mínimo (dBm)'),
                "Ruído Máximo": self._t('noise.max_noise', 'Ruído Máximo (dBm)'),
                "Hora Ruído Máximo": self._t('noise.max_noise_time', 'Hora Ruído Máximo'),
                "Data/Hora": self._t('noise.date_time', 'Data/Hora'),
                "Severidade": self._t('noise.severity', 'Severidade'),
            }
            for column, label in headings.items():
                if column in ("Plot", "ID"):
                    suffix = " ↕"
                else:
                    suffix = " ↕"
                try:
                    self.history_tree.heading(column, text=f"{label}{suffix}", command=lambda col=column: self.sort_treeview(col))
                except Exception as e:
                    print(f"⚠️ NoiseModule: Erro ao atualizar heading '{column}': {e}")
        except Exception as exc:
            print(f"⚠️ NoiseModule: Falha ao atualizar cabeçalhos do histórico: {exc}")

    def _build_sidebar(self):
        """Constrói a barra lateral com todas as seções"""
        # 1. Configuração do Teste
        self._build_test_config_section()
        
        # 2. Controles
        self._build_controls_section()

    def _build_test_config_section(self):
        """Seção de configuração do teste"""
        self.config_frame = ttk.LabelFrame(self.sidebar, text=self._t('noise.test_config', 'Configuração do Teste'), padding=10)
        self.config_frame.pack(fill="x", padx=5, pady=5)
        
        # Nome do teste
        name_frame = ttk.Frame(self.config_frame)
        name_frame.pack(fill="x", pady=2)
        self.test_name_label = ttk.Label(name_frame, text=self._t('noise.name', 'Nome:'))
        self.test_name_label.pack(anchor="w")
        self.test_name_entry = ttk.Entry(name_frame, textvariable=self.test_name_var, width=20)
        self.test_name_entry.pack(fill="x", pady=(2, 0))
        
        # Tempo de varredura
        time_frame = ttk.Frame(self.config_frame)
        time_frame.pack(fill="x", pady=2)
        self.scan_time_label = ttk.Label(time_frame, text=self._t('noise.time_s', 'Tempo (s):'))
        self.scan_time_label.pack(anchor="w")
        self.scan_time_entry = ttk.Entry(time_frame, textvariable=self.scan_time_var, width=20)
        self.scan_time_entry.pack(fill="x", pady=(2, 0))
        
        # Modo de execução
        mode_frame = ttk.Frame(self.config_frame)
        mode_frame.pack(fill="x", pady=2)
        self.mode_label = ttk.Label(mode_frame, text=self._t('noise.mode', 'Modo:'))
        self.mode_label.pack(anchor="w")
        
        mode_options_frame = ttk.Frame(mode_frame)
        mode_options_frame.pack(fill="x", pady=(2, 0))
        
        self.radio_single = ttk.Radiobutton(mode_options_frame, text=self._t('noise.single', 'Único'), variable=self.run_mode_var, value="Single")
        self.radio_single.pack(side="left")
        self.radio_continuous = ttk.Radiobutton(mode_options_frame, text=self._t('noise.continuous', 'Contínuo'), variable=self.run_mode_var, value="Continuous")
        self.radio_continuous.pack(side="left", padx=(10, 0))

    def _build_controls_section(self):
        """Seção de controles"""
        self.controls_frame = ttk.LabelFrame(self.sidebar, text=self._t('noise.controls', 'Controles'), padding=10)
        self.controls_frame.pack(fill="x", padx=5, pady=5)
        
        # Botão de teste
        self.btn_start = ttk.Button(self.controls_frame, text=self._t('noise.test', 'Testar'), command=self.start_scan, state="disabled")
        self.btn_start.pack(fill="x", pady=2)
        
        # Botão de parar
        self.btn_stop = ttk.Button(self.controls_frame, text=self._t('noise.stop', 'Parar'), command=self.stop_scan, state="disabled")
        self.btn_stop.pack(fill="x", pady=2)
        
        # Botão de limpar
        self.btn_clear_plot = ttk.Button(self.controls_frame, text=self._t('noise.clear_plot', 'Limpar Gráfico'), command=self.clear_plot_and_selection)
        self.btn_clear_plot.pack(fill="x", pady=2)
        
        # Botão de salvar
        self.btn_save_selected = ttk.Button(self.controls_frame, text=self._t('noise.save_selected', 'Salvar Selecionados'), command=self.save_selected_tests)
        self.btn_save_selected.pack(fill="x", pady=2)
        
        # Botão de importar
        self.btn_import_tests = ttk.Button(self.controls_frame, text=self._t('noise.import_tests', 'Importar Testes'), command=self.import_tests)
        self.btn_import_tests.pack(fill="x", pady=2)
        
        # Botão de relatório PDF
        self.btn_generate_report = ttk.Button(self.controls_frame, text=self._t('noise.report_pdf', 'Relatório Selecionados (PDF)'), command=self.generate_pdf_report, state="disabled")
        self.btn_generate_report.pack(fill="x", pady=2)


    def _build_main_area(self):
        """Constrói a área principal com gráfico e histórico"""
        # Área do gráfico
        self._build_plot_area()
        
        # Área do histórico (se disponível)
        if DATABASE_AVAILABLE:
            self._build_history_area()

    def _build_plot_area(self):
        """Constrói a área do gráfico"""
        plot_container = ttk.Frame(self.main_area)
        plot_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
        plot_container.columnconfigure(0, weight=1)
        plot_container.rowconfigure(1, weight=1)
        
        self.plot_title_label = ttk.Label(plot_container, text=self._t('noise.monitor_title', 'Monitor de Ruído'), font=("Segoe UI", 11, "bold"))
        self.plot_title_label.grid(row=0, column=0, sticky="w", padx=5, pady=(0, 4))
        
        canvas_frame = ttk.Frame(plot_container)
        canvas_frame.grid(row=1, column=0, sticky="nsew")
        canvas_frame.columnconfigure(0, weight=1)
        canvas_frame.rowconfigure(0, weight=1)
        
        # NOVO: Armazena referência ao frame do plot
        self.plot_frame = plot_container
        
        self.fig = Figure(figsize=(6, 4), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.fig.subplots_adjust(right=0.75, bottom=0.15)
        
        self.canvas = FigureCanvasTkAgg(self.fig, master=canvas_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")
        
        # Controles de zoom e pan na parte superior esquerda
        self._setup_zoom_controls(canvas_frame)
        
        self.setup_plot()

    def _build_history_area(self):
        """Constrói a área do histórico"""
        self.history_frame = ttk.LabelFrame(self.main_area, text=self._t('noise.test_history', 'Histórico de Testes'), padding=10)
        self.history_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
        self.history_frame.columnconfigure(0, weight=1)
        
        stats_frame = ttk.Frame(self.history_frame)
        stats_frame.pack(fill="x", pady=(0, 5))
        self.history_stats_label = ttk.Label(stats_frame, text=self._t('noise.stats_loading', 'Estatísticas: Carregando...'), font=("Segoe UI", 9))
        self.history_stats_label.pack(side="left")
        
        # NOVO: Botão de ajuda para severidade (lupa igual ao Simulador)
        self.severity_help_button = ttk.Label(stats_frame, text="🔍", font=("Helvetica", 12), cursor="hand2", foreground="#0066cc")
        self.severity_help_button.pack(side="left", padx=(10, 0))
        self.severity_help_button.bind('<Button-1>', lambda e: self.show_severity_help())

        history_tree_frame = ttk.Frame(self.history_frame)
        history_tree_frame.pack(fill="both", expand=True, pady=(5, 0))
        
        columns = ("Plot", "ID", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade")
        display_columns = ("Plot", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade")
        
        self.history_tree = ttk.Treeview(history_tree_frame, columns=columns, show="headings", height=6, displaycolumns=display_columns)
        
        self.history_tree.heading("Plot", text="Plot ↕", command=lambda: self.sort_treeview("Plot"))
        self.history_tree.column("Plot", width=50, anchor="center")
        self.history_tree.heading("ID", text="ID ↕", command=lambda: self.sort_treeview("ID"))
        self.history_tree.column("ID", width=50, anchor="center")
        self.history_tree.heading("Nome", text="Nome do Teste ↕", command=lambda: self.sort_treeview("Nome"))
        self.history_tree.column("Nome", width=150, anchor="w")
        self.history_tree.heading("Duração", text="Duração (s) ↕", command=lambda: self.sort_treeview("Duração"))
        self.history_tree.column("Duração", width=100, anchor="center")
        self.history_tree.heading("Ruído Médio", text="Ruído Médio (dBm) ↕", command=lambda: self.sort_treeview("Ruído Médio"))
        self.history_tree.column("Ruído Médio", width=120, anchor="center")
        self.history_tree.heading("Ruído Mínimo", text="Ruído Mínimo (dBm) ↕", command=lambda: self.sort_treeview("Ruído Mínimo"))
        self.history_tree.column("Ruído Mínimo", width=120, anchor="center")
        self.history_tree.heading("Ruído Máximo", text="Ruído Máximo (dBm) ↕", command=lambda: self.sort_treeview("Ruído Máximo"))
        self.history_tree.column("Ruído Máximo", width=120, anchor="center")
        self.history_tree.heading("Hora Ruído Máximo", text="Hora Ruído Máximo ↕", command=lambda: self.sort_treeview("Hora Ruído Máximo"))
        self.history_tree.column("Hora Ruído Máximo", width=140, anchor="center")
        self.history_tree.heading("Data/Hora", text="Data/Hora ↕", command=lambda: self.sort_treeview("Data/Hora"))
        self.history_tree.column("Data/Hora", width=150, anchor="center")
        self.history_tree.heading("Severidade", text="Severidade ↕", command=lambda: self.sort_treeview("Severidade"))
        self.history_tree.column("Severidade", width=80, anchor="center")
        
        history_scrollbar = ttk.Scrollbar(history_tree_frame, orient="vertical", command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=history_scrollbar.set)
        self.history_tree.pack(side="left", fill="both", expand=True)
        history_scrollbar.pack(side="right", fill="y")
        
        history_actions_frame = ttk.Frame(self.history_frame)
        history_actions_frame.pack(fill="x", pady=(5, 0))

        self.select_all_button = ttk.Button(history_actions_frame, text=self._t('noise.select_all', 'Selecionar Todos'), command=self.select_all_tests, width=18)
        self.select_all_button.pack(side="left", padx=(0, 5))
        self.deselect_all_button = ttk.Button(history_actions_frame, text=self._t('noise.deselect_all', 'Deselecionar Todos'), command=self.deselect_all_tests, width=18)
        self.deselect_all_button.pack(side="left", padx=(0, 5))
        self.delete_selected_button = ttk.Button(history_actions_frame, text=self._t('noise.delete_selected', 'Excluir Selecionados...'), command=self.delete_selected_tests, width=20)
        self.delete_selected_button.pack(side="left", padx=(0, 5))
        
        # Eventos do treeview
        self.history_tree.bind('<Button-1>', self.on_history_tree_click)
        
        # NOVO: Sistema de clique/duplo-clique com timer
        self._click_timer = None
        self._click_count = 0
        
        self.update_history_stats()
        self.load_history_to_tree()
        self.after(100, self.auto_plot_selected_tests)

    def _setup_browser_mode_message(self):
        """Configura mensagem de modo browser"""
        # --- Mensagem de Modo Browser ---
        # Posiciona dentro da seção "Configuração do Teste" se existir
        target_parent = getattr(self, 'config_frame', self)
        self.browser_frame = ttk.Frame(target_parent)
        
        # Bullet point azul
        bullet_label = ttk.Label(self.browser_frame, text="•", foreground="blue", font=("Helvetica", 10))
        bullet_label.pack(side='left', anchor='w')
        
        # Texto "modo browser"
        self.browser_mode_label = ttk.Label(self.browser_frame, text=self._t('noise.browser_mode', ' modo browser'), foreground="blue", font=("Helvetica", 10))
        self.browser_mode_label.pack(side='left', anchor='w')
        
        # CORREÇÃO: Controle inicial da visibilidade baseado na licença
        print(f"🔍 NoiseModule: Status inicial da licença: {self.is_licensed}")
        if self.is_licensed:
            self.browser_frame.pack_forget()  # Esconde quando há licença
            print("✅ NoiseModule: Mensagem de modo browser ocultada (licença ativa)")
        else:
            self.browser_frame.pack(fill="x", pady=(5, 5), padx=5)  # Mostra quando não há licença
            print("⚠️ NoiseModule: Mensagem de modo browser visível (sem licença)")

    def update_license_status(self, new_license_status):
        """CORREÇÃO: Atualiza o status da licença e a interface do usuário"""
        try:
            print(f"🔔 NoiseModule: Atualizando status da licença de {self.is_licensed} para {new_license_status}")
            
            # CORREÇÃO: Verifica se a licença é realmente válida para o módulo Noise
            # Mesmo que o app_shell diga que há licença, verifica se é específica para Noise
            if new_license_status and self.app_shell:
                try:
                    # Verifica se há licença específica para Noise
                    valid_lics = ["NoiseCheck", "FastChecker"]
                    license_limits = self.app_shell._calculate_license_limits(valid_lics)
                    if not license_limits.get('is_licensed', False):
                        print("⚠️ NoiseModule: AppShell diz que há licença, mas não é válida para Noise")
                        new_license_status = False
                    else:
                        print(f"✅ NoiseModule: Licença válida confirmada: {license_limits.get('license_name', 'N/A')}")
                except Exception as e:
                    print(f"⚠️ NoiseModule: Erro ao verificar licença específica: {e}")
                    new_license_status = False
            
            # Atualiza o status interno
            self.is_licensed = new_license_status
            
            # Atualiza o status inicial
            if new_license_status:
                self._set_status('noise.status_ready', "Pronto para iniciar teste de ruído")
            else:
                self._set_status('noise.status_browser_mode', "Modo browser - funcionalidade limitada")
            
            # Atualiza a interface
            self._update_ui_for_license_status()
            
            print(f"✅ NoiseModule: Status da licença atualizado para: {self.is_licensed}")
            
        except Exception as e:
            print(f"❌ Erro ao atualizar status da licença no NoiseModule: {e}")
    
    def _update_ui_for_license_status(self):
        """CORREÇÃO: Atualiza a interface baseada no status da licença (padrão Antenna)"""
        try:
            # CORREÇÃO: Verifica se os widgets já foram criados
            if not hasattr(self, 'browser_frame'):
                print("⚠️ NoiseModule: Widgets ainda não criados, aguardando...")
                return
            
            print(f"🔍 NoiseModule: Atualizando interface para licença: {self.is_licensed}")
            
            # CORREÇÃO: Controla visibilidade do frame de modo browser (como no módulo Antenna)
            if self.is_licensed:
                # Esconde mensagem de modo browser quando há licença
                if hasattr(self, 'browser_frame'):
                    try:
                        self.browser_frame.pack_forget()
                    except tk.TclError:
                        self.browser_frame.grid_forget()
                    print("✅ NoiseModule: Frame de modo browser ocultado")
            else:
                # Mostra mensagem de modo browser quando não há licença
                if hasattr(self, 'browser_frame'):
                    if not self.browser_frame.winfo_manager():
                        try:
                            self.browser_frame.pack(fill="x", pady=(5, 5), padx=5)
                        except tk.TclError:
                            self.browser_frame.grid(row=1, column=0, sticky="ew", pady=(5, 5), padx=5)
                    print("✅ NoiseModule: Frame de modo browser exibido")
            
            # Atualiza o status dos botões
            if hasattr(self, 'btn_start'):
                self.btn_start.config(state='normal' if self.is_licensed else 'disabled')
            
            if hasattr(self, 'btn_stop'):
                self.btn_stop.config(state='normal' if self.is_licensed else 'disabled')
            
            # Ações de browser devem permanecer habilitadas em modo browser
            if hasattr(self, 'btn_clear_plot'):
                self.btn_clear_plot.config(state='normal')
            if hasattr(self, 'btn_save_selected'):
                self.btn_save_selected.config(state='normal')
            if hasattr(self, 'btn_import_tests'):
                self.btn_import_tests.config(state='normal')
            
            # Atualiza campos de entrada
            if hasattr(self, 'test_name_entry'):
                self.test_name_entry.config(state='normal' if self.is_licensed else 'disabled')
            
            if hasattr(self, 'scan_time_entry'):
                self.scan_time_entry.config(state='normal' if self.is_licensed else 'disabled')
            
            if hasattr(self, 'run_mode_combobox'):
                self.run_mode_combobox.config(state='readonly' if self.is_licensed else 'disabled')
            
            # Atualiza botões do histórico (liberados em modo browser)
            if hasattr(self, 'delete_selected_button'):
                self.delete_selected_button.config(state='normal')
            
            print(f"✅ NoiseModule: Interface atualizada para status da licença: {self.is_licensed}")
            
        except Exception as e:
            print(f"❌ Erro ao atualizar interface do NoiseModule: {e}")

    def get_scan_duration(self):
        try: return int(self.scan_time_var.get()) if int(self.scan_time_var.get()) > 0 else 60
        except ValueError: return 60

    def check_duplicate_test_name(self, test_name):
        """
        Verifica se o nome do teste já existe no histórico
        
        Args:
            test_name: Nome do teste a verificar
            
        Returns:
            bool: True se o nome já existe, False caso contrário
        """
        if not DATABASE_AVAILABLE or not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            existing_names = [test.get('test_name', '').strip().lower() for test in history]
            return test_name_normalized in existing_names
        except Exception as e:
            print(f"❌ Erro ao verificar nomes duplicados: {e}")
            return False

    def check_duplicate_test_name_excluding_current(self, test_name, current_test_id):
        """
        Verifica se o nome do teste já existe no histórico, excluindo o teste atual
        
        Args:
            test_name: Nome do teste a verificar
            current_test_id: ID do teste atual que está sendo editado
            
        Returns:
            bool: True se o nome já existe em outro teste, False caso contrário
        """
        if not DATABASE_AVAILABLE or not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            
            for test in history:
                if test.get('id') != current_test_id:  # Exclui o teste atual
                    existing_name = test.get('test_name', '').strip().lower()
                    if existing_name == test_name_normalized:
                        return True
            return False
        except Exception as e:
            print(f"❌ Erro ao verificar nomes duplicados: {e}")
            return False

    def get_test_color(self, test_id):
        """
        Gera uma cor fixa e consistente baseada no ID do teste
        
        Args:
            test_id: ID único do teste
            
        Returns:
            str: Código hexadecimal da cor
        """
        # Paleta de cores com alto contraste e boa distinção visual
        colors = [
            '#1f77b4',  # Azul
            '#d62728',  # Vermelho
            '#2ca02c',  # Verde
            '#ff7f0e',  # Laranja
            '#9467bd',  # Roxo
            '#8c564b',  # Marrom
            '#e377c2',  # Rosa
            '#7f7f7f',  # Cinza
            '#bcbd22',  # Verde-amarelado
            '#17becf',  # Ciano
            '#aec7e8',  # Azul claro
            '#ffbb78',  # Laranja claro
            '#98df8a',  # Verde claro
            '#ff9896',  # Vermelho claro
            '#c5b0d5',  # Roxo claro
            '#c49c94',  # Marrom claro
            '#f7b6d3',  # Rosa claro
            '#c7c7c7',  # Cinza claro
            '#dbdb8d',  # Amarelo claro
            '#9edae5'   # Ciano claro
        ]
        
        # Converte test_id para string para usar como chave
        test_key = str(test_id)
        
        # Se já existe uma cor atribuída a este teste, retorna ela
        if test_key in self.test_colors:
            return self.test_colors[test_key]
        
        # Se não existe, atribui uma nova cor baseada no ID
        if isinstance(test_id, (int, str)):
            try:
                test_id_int = int(test_id)
                color = colors[test_id_int % len(colors)]
            except (ValueError, TypeError):
                # Se ID inválido, usa hash do ID como string
                hash_value = hash(str(test_id)) % len(colors)
                color = colors[hash_value]
        else:
            # Fallback para IDs não numéricos
            hash_value = hash(str(test_id)) % len(colors)
            color = colors[hash_value]
        
        # Salva a cor atribuída para persistência
        self.test_colors[test_key] = color
        self._save_test_colors()
        return color
    
    def _load_test_colors(self):
        """Carrega as cores persistentes dos testes"""
        try:
            if STATE_PERSISTENCE_AVAILABLE and self.state_persistence:
                colors_data = self.state_persistence.get_state_value('test_colors', {})
                if isinstance(colors_data, dict):
                    self.test_colors = colors_data
                    print(f"✅ Cores dos testes carregadas: {len(self.test_colors)} cores")
        except Exception as e:
            print(f"⚠️ Erro ao carregar cores dos testes: {e}")
            self.test_colors = {}
    
    def _save_test_colors(self):
        """Salva as cores persistentes dos testes"""
        try:
            if STATE_PERSISTENCE_AVAILABLE and self.state_persistence:
                self.state_persistence.set_state_value('test_colors', self.test_colors)
        except Exception as e:
            print(f"⚠️ Erro ao salvar cores dos testes: {e}")

    def get_dynamic_x_scale(self):
        """
        Calcula a escala dinâmica do eixo X baseada nos dados sendo plotados
        
        Returns:
            tuple: (x_min, x_max) para o eixo X
        """
        max_time = 0
        active_data_sources = 0
        
        # Verifica dados ao vivo
        if self.live_data:
            live_times = [float(t) for t in self.live_data.keys()]
            if live_times:
                max_time = max(max_time, max(live_times))
                active_data_sources += 1
        
        # Verifica múltiplos testes do histórico
        if self.multiple_tests:
            for test_name, test_info in self.multiple_tests.items():
                if test_info:
                    # NOVO: Verifica se é a nova estrutura com ID
                    if isinstance(test_info, dict) and 'data' in test_info and 'id' in test_info:
                        test_data = test_info['data']
                    else:
                        # Estrutura antiga - mantém compatibilidade
                        test_data = test_info
                    
                    if test_data:
                        test_times = [float(t) for t in test_data.keys()]
                        if test_times:
                            max_time = max(max_time, max(test_times))
                            active_data_sources += 1
        
        # Verifica dados importados
        if self.imported_data:
            imported_times = [float(t) for t in self.imported_data.keys()]
            if imported_times:
                max_time = max(max_time, max(imported_times))
                active_data_sources += 1
        
        # Se não há dados, usa a duração configurada
        if max_time == 0:
            max_time = self.get_scan_duration()
        
        # NOVO: Ajusta margem baseada no número de fontes de dados
        if active_data_sources == 1:
            # Apenas um teste: margem menor para melhor visualização
            margin = max(2, max_time * 0.02)  # 2% ou mínimo 2 segundos
        else:
            # Múltiplos testes: margem maior para acomodar todos
            margin = max(5, max_time * 0.05)  # 5% ou mínimo 5 segundos
        
        return 0, max_time + margin

    def setup_plot(self):
        self.ax.clear()
        self.ax.set_xlabel("Tempo (s)"); self.ax.set_ylabel("Intensidade (dBm)")
        self.ax.grid(True, which='both', linestyle='--', linewidth=0.5)
        
        # NOVO: Usa escala dinâmica do eixo X
        x_min, x_max = self.get_dynamic_x_scale()
        self.ax.set_xlim(x_min, x_max)
        self.ax.set_ylim(-80, -40)
        
        self.annot = self.ax.annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
                                       bbox=dict(boxstyle="round,pad=0.5", 
                                                facecolor="lightblue", 
                                                alpha=0.9,
                                                edgecolor="navy",
                                                linewidth=1),
                                       arrowprops=dict(arrowstyle="->", 
                                                      connectionstyle="arc3,rad=0",
                                                      color="navy",
                                                      lw=1),
                                       fontsize=9, 
                                       ha='left',
                                       va='bottom',
                                       wrap=True)
        self.annot.set_visible(False)
        self.setup_interactivity()

    def setup_interactivity(self):
        if hasattr(self, 'canvas'):
            self.canvas.mpl_connect('motion_notify_event', self._on_hover)

    def _on_hover(self, event):
        if event.inaxes != self.ax:
            if self.annot and self.annot.get_visible():
                self.annot.set_visible(False); self.canvas.draw_idle()
            return
        for line in self.ax.lines:
            cont, ind = line.contains(event)
            if cont:
                pos = line.get_xydata()[ind["ind"][0]]
                x_coord, y_coord = pos[0], pos[1]
                tooltip_text = f"{x_coord:.2f}s -> {y_coord:.2f} dBm"
                self._position_tooltip_smartly(event, x_coord, y_coord, tooltip_text)
                return
        if self.annot and self.annot.get_visible():
            self.annot.set_visible(False); self.canvas.draw_idle()

    def _position_tooltip_smartly(self, event, x_pos, y_pos, tooltip_text):
        """Posiciona o tooltip de forma inteligente baseado na posição do mouse"""
        try:
            # Obtém dimensões do canvas em pixels
            canvas_width = self.canvas.get_width_height()[0]
            canvas_height = self.canvas.get_width_height()[1]
            
            # Converte coordenadas do evento para pixels do canvas
            x_pixel = event.x
            y_pixel = event.y
            
            # Margens mais conservadoras para evitar cortes
            margin_right = 300   # Margem para borda direita
            margin_top = 150     # Margem para borda superior
            margin_bottom = 150  # Margem para borda inferior
            margin_left = 250    # Margem para borda esquerda
            
            # Calcula posição do tooltip baseada na posição do mouse
            if x_pixel > canvas_width - margin_right:  # Próximo à borda direita
                # Posiciona à esquerda do mouse com margem maior
                xytext = (-150, 20)
                ha = 'right'
            elif x_pixel < margin_left:  # Próximo à borda esquerda
                # Posiciona à direita do mouse com margem maior
                xytext = (150, 20)
                ha = 'left'
            else:  # Posição normal (centro)
                # Posiciona à direita do mouse
                xytext = (20, 20)
                ha = 'left'
            
            # Ajusta posição vertical se necessário
            if y_pixel < margin_top:  # Próximo ao topo
                xytext = (xytext[0], 150)  # Margem maior para baixo
            elif y_pixel > canvas_height - margin_bottom:  # Próximo à parte inferior
                xytext = (xytext[0], -150)  # Margem maior para cima
            
            # Atualiza o tooltip com posicionamento inteligente
            self.annot.xy = (x_pos, y_pos)
            self.annot.xytext = xytext
            self.annot.set_text(tooltip_text)
            self.annot.set_ha(ha)
            self.annot.set_visible(True)
            
            # Força redesenho para garantir que o tooltip apareça
            self.canvas.draw_idle()
            
        except Exception as e:
            print(f"⚠️ Erro no posicionamento do tooltip: {e}")
            # Fallback para posicionamento padrão
            try:
                self.annot.xy = (x_pos, y_pos)
                self.annot.set_text(tooltip_text)
                self.annot.set_visible(True)
                self.canvas.draw_idle()
            except Exception as fallback_error:
                print(f"⚠️ Erro no fallback do tooltip: {fallback_error}")

    def update_plot(self):
        self.ax.clear(); self.setup_plot()
        all_data_plotted = False
        if self.imported_data:
            times = list(self.imported_data.keys()); values = list(self.imported_data.values())
            self.ax.plot(times, values, 'g-', marker='o', markersize=2, linewidth=0.8, alpha=0.6, label='Dados Importados', picker=5)
            all_data_plotted = True
        if self.multiple_tests:
            for test_name, test_info in self.multiple_tests.items():
                if test_info:
                    # NOVO: Verifica se é a nova estrutura com ID
                    if isinstance(test_info, dict) and 'data' in test_info:
                        test_data = test_info['data']
                    else:
                        # Estrutura antiga - mantém compatibilidade
                        test_data = test_info
                    
                    # Usa SEMPRE uma cor consistente baseada no ID do teste,
                    # garantindo que a cor não mude entre sessões.
                    test_id = test_info.get('id') if isinstance(test_info, dict) else None
                    if test_id:
                        color = self.get_test_color(test_id)
                    else:
                        # Fallback para estrutura antiga - usa hash do nome
                        color = self.get_test_color(hash(test_name))
                    
                    if test_data and len(test_data) > 0:
                        times = list(test_data.keys())
                        values = list(test_data.values())
                        self.ax.plot(times, values, '-', marker='o', markersize=2, linewidth=0.8, alpha=0.7, label=test_name, picker=5, color=color)
                        all_data_plotted = True
        if self.live_data:
            times = list(self.live_data.keys())
            # NOVO: Extrai valores da nova estrutura de dados
            if isinstance(list(self.live_data.values())[0], dict):
                values = [v["value"] for v in self.live_data.values()]
            else:
                values = list(self.live_data.values())
            # Usa a mesma cor baseada no ID do teste atual (se disponível)
            # Para teste em tempo real, usa um ID temporário baseado no nome
            if self.current_test_name:
                # Gera um ID temporário consistente baseado no nome do teste
                temp_id = abs(hash(self.current_test_name)) % 1000
                live_color = self.get_test_color(temp_id)
            else:
                live_color = '#d62728'  # Vermelho padrão
            self.ax.plot(times, values, '-', marker='o', markersize=2, linewidth=0.8, alpha=0.9, label=self.current_test_name, picker=5, color=live_color)
            all_data_plotted = True
        
        # NOVO: Recalcula e ajusta a escala do eixo X após plotar todos os dados
        if all_data_plotted:
            x_min, x_max = self.get_dynamic_x_scale()
            self.ax.set_xlim(x_min, x_max)
            self.ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', 
                          bbox_transform=self.ax.transAxes, frameon=True, 
                          borderpad=1.0, columnspacing=1.0, fontsize=9)
            
            # NOVO: Atualiza status com informação da escala
            if hasattr(self, 'status_var'):
                test_count = len(self.multiple_tests) if self.multiple_tests else 0
                if test_count == 1:
                    self._set_status('noise.status_scale_single', "Escala X: 0-{max_time:.1f}s (teste único)", max_time=x_max)
                elif test_count > 1:
                    self._set_status('noise.status_scale_multiple', "Escala X: 0-{max_time:.1f}s ({count} testes)", max_time=x_max, count=test_count)
                else:
                    self._set_status('noise.status_scale', "Escala X: 0-{max_time:.1f}s", max_time=x_max)
        
        self.canvas.draw()

    def update_ui_state(self, is_running):
        # Estado base para ações de browser (independente de licença)
        browser_btn_state = 'disabled' if is_running else 'normal'

        # Ações que acionam o reader dependem de licença
        self.btn_start.config(state='disabled' if is_running else ('normal' if self.is_licensed else 'disabled'))
        self.btn_stop.config(state='normal' if is_running else 'disabled')
        
        # CORREÇÃO: Botões "Limpar Gráfico" e "Importar Testes" sempre habilitados
        self.btn_clear_plot.config(state='normal')
        self.btn_import_tests.config(state='normal')
        
        # Ações de browser: manter habilitadas independentemente da licença
        if hasattr(self, 'btn_save_selected'):
            self.btn_save_selected.config(state=browser_btn_state)
        if hasattr(self, 'delete_selected_button'):
            self.delete_selected_button.config(state=browser_btn_state)
        
        if DATABASE_AVAILABLE:
            for child in self.winfo_children():
                if isinstance(child, ttk.Button) and child.cget('text') in ["Selecionar Todos", "Deselecionar Todos", "Excluir Selecionados..."]:
                    child.config(state='normal')
        


        # Controle dos campos: em modo browser, permitir edição (não aciona reader)
        if self.is_licensed:
            # Com licença: campos habilitados baseado no estado do teste (se estiver executando, travar)
            for child in [self.radio_single, self.radio_continuous, self.scan_time_entry, self.test_name_entry]:
                child.config(state='disabled' if is_running else 'normal')
        else:
            # Sem licença: liberar campos para edição
            for child in [self.radio_single, self.radio_continuous, self.scan_time_entry, self.test_name_entry]:
                child.config(state='normal')
            
    def start_scan(self):
        if self.demo_mode:
            messagebox.showinfo(
                self._t('noise.demo_mode', "Modo Demo"),
                self._t('noise.demo_mode_msg', "Este módulo está funcionando em modo demo.\n\nPara funcionalidade completa, adicione uma licença válida no módulo License."),
                parent=self
            )
            return
        if not self.is_licensed:
            messagebox.showwarning(
                self._t('noise.invalid_license', "Licença Inválida"),
                self._t('noise.invalid_license_msg', "Funcionalidade desabilitada."),
                parent=self
            )
            return
        
        # NOVO: Validação dos limites da licença
        if not self.license_limits.get('is_licensed', False):
            messagebox.showerror(
                self._t('noise.invalid_license', "Licença Inválida"),
                self._t('noise.invalid_license_not_found', "Nenhuma licença ativa encontrada.\n\nPara usar este módulo, ative uma licença válida no módulo License."),
                parent=self
            )
            return
        
        # NOVO: Validação da frequência fixa (915 MHz) contra limites da licença
        fixed_freq = FIXED_FREQ_MHZ  # 915.0 MHz
        min_freq = self.license_limits.get('min_freq', 800)
        max_freq = self.license_limits.get('max_freq', 1000)
        
        if not (min_freq <= fixed_freq <= max_freq):
            messagebox.showerror(
                self._t('noise.freq_outside_license', "Frequência fora da faixa da licença"),
                self._t(
                    'noise.freq_outside_license_msg',
                    "A frequência de operação do Noise Check ({freq} MHz) está fora da faixa permitida pela licença.\n\nFaixa permitida: {min_freq} - {max_freq} MHz\nFrequência do módulo: {freq} MHz\n\nEste módulo não pode operar com a licença atual.",
                    freq=fixed_freq,
                    min_freq=min_freq,
                    max_freq=max_freq
                ),
                parent=self
            )
            return
        temp = self.get_temperature()
        if temp is not None and temp >= TEMPERATURE_LIMIT:
            messagebox.showerror(
                self._t('noise.safety_stop_temp', "Parada de Segurança"),
                self._t('noise.safety_stop_temp_msg', "Temperatura ({temp}°C) excedeu o limite de {limit}°C.", temp=temp, limit=TEMPERATURE_LIMIT),
                parent=self
            )
            return
        
        # NOVO: Validação de nome duplicado
        self.current_test_name = self.test_name_var.get().strip()
        if not self.current_test_name: 
            self.current_test_name = f"Noise_{len(self.multiple_tests) + 1}"
        
        # Verifica se o nome já existe no histórico
        if self.check_duplicate_test_name(self.current_test_name):
            messagebox.showerror(
                self._t('noise.test_name_duplicate', "Nome Duplicado"),
                self._t('noise.test_name_duplicate_msg', "Já existe um teste com o nome '{name}' no histórico.\n\nPor favor, escolha um nome diferente para este teste.", name=self.current_test_name),
                parent=self
            )
            return
        self.live_data = {}
        self.test_start_time = datetime.now()  # NOVO: Registra horário de início
        self.test_is_running = True; self.stop_requested = False
        if self.app_shell: self.app_shell.set_test_running(True, "Noise Check")
        self.update_ui_state(True)
        self.worker_thread = threading.Thread(target=self.worker_scan, daemon=True); self.worker_thread.start()

    def stop_scan(self):
        self._set_status('noise.status_stop_requested', "Parada solicitada...")
        self.stop_requested = True
        self.test_is_running = False
        if self.live_data and self.current_test_name:
            # NOVO: Converte dados para formato compatível com múltiplos testes
            if isinstance(list(self.live_data.values())[0], dict):
                converted_data = {t: v["value"] for t, v in self.live_data.items()}
            else:
                converted_data = self.live_data.copy()
            self.multiple_tests[self.current_test_name] = converted_data
            self.save_test_to_history()
        if self.app_shell: self.app_shell.set_test_running(False, "Noise Check")
        
    def worker_scan(self):
        try:
            scan_duration, run_mode = self.get_scan_duration(), self.run_mode_var.get()
            ### ALTERADO: usa self.com_port ###
            if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) != 0:
                self._set_status('noise.status_error_open_com', "Erro CRÍTICO: Falha ao abrir a porta COM{port}", port=self.com_port)
                self.after(0, self.update_ui_state, False)
                return
            self._set_status('noise.status_config_frequency', "Configurando frequência para {freq} MHz...", freq=FIXED_FREQ_MHZ)
            dummy_buffer, dummy_len = ctypes.create_string_buffer(256), ctypes.c_uint(0)
            freq_data = (1).to_bytes(1, 'big') + int(FIXED_FREQ_MHZ * 1000).to_bytes(3, 'big')
            rfid_sdk.UHF_RFID_Set(COMMAND_CODE_SET_FREQ, ctypes.c_char_p(freq_data), 4, dummy_buffer, ctypes.byref(dummy_len))
            time.sleep(0.1)
            start_time = time.time()
            self._set_status('noise.status_starting_measurement', "Iniciando medição...")
            while self.test_is_running:
                current_time = time.time(); elapsed_time = current_time - start_time
                if run_mode == "Single" and elapsed_time > scan_duration: 
                    self.test_is_running = False; 
                    if self.live_data and self.current_test_name: self.save_test_to_history()
                    break
                # Modo Contínuo - mede indefinidamente até ser parado pelo operador
                if run_mode == "Continuous":
                    # Não há condição de parada automática - continua até stop_scan() ser chamado
                    # Mantém os dados acumulados sem resetar
                    pass
                output_buffer, output_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
                status_noise = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_RSSIVALU, bytes([0,0]), 2, output_buffer, ctypes.byref(output_len))
                noise_dbm = 0.0
                if status_noise == 0 and output_len.value >= 3:
                    noise_dbm = Decimal(struct.unpack('>h', output_buffer.raw[1:3])[0]) / Decimal(10.0)
                    # NOVO: Registra timestamp absoluto junto com o valor
                    absolute_time = self.test_start_time + timedelta(seconds=elapsed_time)
                    self.live_data[elapsed_time] = {
                        "value": float(noise_dbm),
                        "absolute_time": absolute_time.strftime("%H:%M:%S")  # Formato HH:MM:SS (sem milissegundos)
                    }
                now = time.time()
                if (now - self._last_plot_update) >= self._plot_update_interval:
                    self._last_plot_update = now
                    self.after(0, self.update_plot)
                if run_mode == "Continuous":
                    self._set_status(
                        'noise.status_continuous',
                        "Medição Contínua... Tempo: {elapsed:.1f}s | Intensidade: {value:.2f} dBm | Pressione PARAR para finalizar",
                        elapsed=elapsed_time,
                        value=float(noise_dbm)
                    )
                else:
                    self._set_status(
                        'noise.status_running',
                        "Medindo... Tempo: {elapsed:.1f}s | Intensidade: {value:.2f} dBm",
                        elapsed=elapsed_time,
                        value=float(noise_dbm)
                    )
                time.sleep(0.05)
        except Exception as e:
            self._set_status('noise.status_execution_error', "Erro durante a execução: {error}", error=e)
        finally:
            ### ALTERADO: usa self.com_port ###
            if rfid_sdk: rfid_sdk.UHF_RFID_Close(self.com_port)
            self.after(0, self.update_plot)
            if self.stop_requested:
                self._set_status('noise.status_measurement_stopped', "Medição parada.")
            else:
                self._set_status('noise.status_measurement_finished', "Medição finalizada.")
            if self.app_shell: self.app_shell.set_test_running(False, "Noise Check")
            self.after(0, self.update_ui_state, False)

    def clear_plot_and_selection(self):
        self.live_data.clear(); self.imported_data.clear(); self.multiple_tests.clear()
        self.current_test_name = ""
        self.setup_plot(); self.update_plot()
        
        if DATABASE_AVAILABLE and hasattr(self, 'history_tree'):
            self.deselect_all_tests()
        
                    # CORREÇÃO: Status contextual baseado na licença
            is_licensed = self.is_licensed
            if is_licensed:
                self._set_status('noise.status_plot_cleared_ready', "Gráfico limpo. Pronto para iniciar.")
            else:
                self._set_status('noise.status_plot_cleared_browser', "Gráfico limpo. Modo browser ativo.")

    def save_selected_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        selected_ids = []
        for item in self.history_tree.get_children():
            if self.history_tree.set(item, 'Plot') == "☑":
                selected_ids.append(self.history_tree.item(item)['values'][1])
        
        if not selected_ids:
            messagebox.showwarning(
                self._t('noise.no_test_selected_save', "Nenhum Teste Selecionado"),
                self._t('noise.no_test_selected_save_msg', "Por favor, selecione um ou mais testes no histórico para salvar."),
                parent=self
            )
            return

        tests_to_save = [self.database.get_test_by_id(test_id) for test_id in selected_ids]
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        default_filename = f"Noise_tests_{timestamp}.json"
        
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json")],
            title=self._t('noise.save_tests_title', "Salvar Testes Selecionados"),
            initialfile=default_filename,
            parent=self
        )
        if not filepath: return
            
        try:
            export_data = {
                "metadata": { "export_date": datetime.now().strftime('%d-%m-%Y %H:%M:%S'), "source": "Noise Module - FastChecker II" },
                "test_data": tests_to_save
            }
            with open(filepath, "w", encoding='utf-8') as f:
                json.dump(export_data, f, indent=2, ensure_ascii=False)
            messagebox.showinfo(
                self._t('noise.success', "Sucesso"),
                self._t('noise.tests_saved_success', "{count} teste(s) salvos com sucesso em:\n{filename}", count=len(tests_to_save), filename=os.path.basename(filepath)),
                parent=self
            )
        except Exception as e: 
            messagebox.showerror(
                self._t('noise.error_saving_tests', "Erro ao Salvar"),
                self._t('noise.error_saving_tests_msg', "Não foi possível salvar os testes.\nErro: {error}", error=e),
                parent=self
            )

    def import_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        # NOVO: Ao importar, apaga o teste ativo (limpa gráfico e dados atuais)
        try:
            if getattr(self, 'test_is_running', False):
                # Solicita parada graciosa
                self.stop_requested = True
                self.test_is_running = False
                if hasattr(self, 'worker_thread') and self.worker_thread and self.worker_thread.is_alive():
                    self.worker_thread.join(timeout=0.5)
                if self.app_shell:
                    self.app_shell.set_test_running(False, "Noise Check")
            # Limpa dados e seleção atuais
            self.clear_plot_and_selection()
        except Exception as _e:
            print(f"⚠️ NoiseModule: Falha ao limpar teste ativo antes da importação: {_e}")

        filepath = filedialog.askopenfilename(
            filetypes=[("JSON Files", "*.json")],
            title=self._t('noise.import_tests_title', "Importar Testes para o Histórico"),
            parent=self
        )
        if not filepath: return
        
        try:
            with open(filepath, "r", encoding='utf-8') as f:
                report_data = json.load(f)
            
            tests_to_import = report_data.get("test_data", [])
            if not tests_to_import:
                messagebox.showwarning(
                    self._t('noise.no_tests_in_file', "Nenhum Teste Encontrado"),
                    self._t('noise.no_tests_in_file_msg', "O arquivo selecionado não contém dados de teste no formato esperado."),
                    parent=self
                )
                return

            imported_count = 0
            for test_data in tests_to_import:
                if 'id' in test_data: del test_data['id']
                test_data['show_in_graph'] = True
                if self.database.add_test_to_history(test_data):
                    imported_count += 1
            
            if imported_count > 0:
                self._set_status('noise.status_import_success', "{count} teste(s) importado(s) com sucesso.", count=imported_count)
                self.load_history_to_tree()
                self.update_plot_from_history()
            else:
                self._set_status('noise.status_import_none', "Nenhum teste foi importado.")

        except Exception as e: 
            messagebox.showerror(
                self._t('noise.import_error', "Erro de Importação"),
                self._t('noise.import_error_msg', "Não foi possível ler o arquivo.\nErro: {error}", error=e),
                parent=self
            )

    def generate_pdf_report(self):
        """Gera relatório PDF com diálogo para salvar arquivo - apenas testes selecionados"""
        try:
            # Verifica se há dados para relatório
            if not DATABASE_AVAILABLE or not self.database:
                messagebox.showwarning(
                    self._t('noise.db_unavailable_report', "Aviso"),
                    self._t('noise.db_unavailable_report_msg', "Banco de dados não disponível para gerar relatório."),
                    parent=self
                )
                return
            
            # Obtém apenas os testes selecionados
            selected_ids = self.get_selected_test_ids()
            if not selected_ids:
                messagebox.showwarning(
                    self._t('noise.no_test_selected_report', "Nenhum Teste Selecionado"),
                    self._t('noise.no_test_selected_report_msg', "Por favor, selecione um ou mais testes no histórico para gerar o relatório."),
                    parent=self
                )
                return
            
            # Carrega apenas os testes selecionados
            history_data = []
            for test_id in selected_ids:
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    history_data.append(test_data)
            
            if not history_data:
                messagebox.showwarning(
                    self._t('noise.no_valid_test', "Aviso"),
                    self._t('noise.no_valid_test_msg', "Nenhum teste válido encontrado entre os selecionados."),
                    parent=self
                )
                return
            
            # Gera nome do arquivo com data e hora no formato solicitado
            now = datetime.now()
            filename = f"Noise_Check_Report_{now.strftime('%d.%m.%y_%H.%M.%S')}.pdf"
            
            # Diálogo para salvar arquivo
            filepath = filedialog.asksaveasfilename(
                defaultextension=".pdf",
                filetypes=[("PDF Files", "*.pdf")],
                initialfile=filename,
                title=self._t('noise.report_pdf', "Relatório Selecionados (PDF)"),
                parent=self
            )
            
            if not filepath:
                return  # Usuário cancelou
            
            # Mostra mensagem de progresso
            self._set_status('noise.status_generating_pdf', "Gerando relatório PDF...", force=True)
            self.update()
            
            # Gera o PDF com dados selecionados
            result = self._generate_pdf_with_selected_tests(filepath, history_data)
            
            if result['success']:
                self._set_status('noise.status_pdf_success', "Relatório PDF dos testes selecionados gerado com sucesso!")
                messagebox.showinfo(
                    self._t('noise.success', "Sucesso"),
                    self._t('noise.success_report_generated', "Relatório PDF dos testes selecionados gerado com sucesso!\n\nArquivo: {filepath}\n\nTestes incluídos: {count}", filepath=filepath, count=len(history_data)),
                    parent=self
                )
                
                # Abre o PDF automaticamente
                try:
                    os.startfile(filepath)
                except Exception:
                    pass  # Ignora erro se não conseguir abrir
            else:
                error_msg = result.get('error', 'Erro desconhecido')
                self._set_status('noise.status_pdf_error', "Erro ao gerar relatório PDF")
                messagebox.showerror(
                    self._t('noise.error_generating_pdf_title', "Erro ao gerar relatório PDF"),
                    self._t('noise.error_generating_pdf', "Erro ao gerar relatório PDF:\n{error}", error=error_msg),
                    parent=self
                )
                
        except Exception as e:
            self._set_status('noise.status_pdf_error', "Erro ao gerar relatório PDF")
            messagebox.showerror(
                self._t('noise.error_generating_pdf_title', "Erro ao gerar relatório PDF"),
                self._t('noise.error_unexpected_pdf', "Erro inesperado ao gerar relatório:\n{error}", error=str(e)),
                parent=self
            )

    def get_selected_test_ids(self):
        """Retorna uma lista com os IDs dos testes selecionados no histórico"""
        if not hasattr(self, 'history_tree'):
            return []
        
        selected_ids = []
        for item in self.history_tree.get_children():
            if self.history_tree.set(item, 'Plot') == "☑":
                test_id = self.history_tree.item(item)['values'][1]
                selected_ids.append(test_id)
        return selected_ids

    def update_report_button_state(self):
        """Atualiza o estado do botão de relatório baseado na seleção de testes"""
        if not hasattr(self, 'btn_generate_report'):
            return
        
        selected_ids = self.get_selected_test_ids()
        has_selection = len(selected_ids) > 0
        
        # O botão só fica ativo se houver testes selecionados
        self.btn_generate_report.config(state='normal' if has_selection else 'disabled')

    def _generate_pdf_with_selected_tests(self, filepath, selected_tests):
        """Gera PDF apenas com os testes selecionados"""
        try:
            return self._generate_pdf_with_reportlab(filepath, selected_tests)
        except ImportError:
            try:
                from fastchecker.pdf_generator import generate_pdf_only
            except ImportError:
                from pdf_generator import generate_pdf_only
            return generate_pdf_only(filepath)
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_pdf_with_reportlab(self, filepath, selected_tests):
        """Gera PDF usando ReportLab com dados selecionados"""
        try:
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.units import inch
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, KeepTogether
            from reportlab.lib import colors
            from reportlab.lib.enums import TA_CENTER, TA_LEFT
            try:
                from .html_report_generator import _calculate_severity  # type: ignore
            except ImportError:
                from html_report_generator import _calculate_severity  # type: ignore
            try:
                from .image_chart_generator import generate_all_charts  # type: ignore
            except ImportError:
                from image_chart_generator import generate_all_charts  # type: ignore

            translator = self.translator
            if not translator and TRANSLATOR_AVAILABLE and callable(get_translator):
                translator = get_translator()
            current_lang = 'pt'
            if translator and hasattr(translator, 'get_language'):
                try:
                    current_lang = (translator.get_language() or 'pt').lower()
                except Exception:
                    current_lang = 'pt'
            lang_prefix = (current_lang or 'pt')[:2]

            # Função para formatar datas conforme idioma atual
            def _format_datetime(value: str) -> str:
                if not value or not isinstance(value, str):
                    return '-'
                try:
                    from datetime import datetime
                    timestamp_dt = None
                    if 'T' in value:
                        timestamp_dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
                    else:
                        formats_to_try = [
                            '%d/%m/%Y %H:%M:%S',
                            '%d/%m/%Y %H:%M',
                            '%d-%m-%Y %H:%M:%S',
                            '%d-%m-%Y %H:%M',
                            '%Y-%m-%d %H:%M:%S',
                            '%Y-%m-%d %H:%M'
                        ]
                        for fmt in formats_to_try:
                            try:
                                timestamp_dt = datetime.strptime(value, fmt)
                                break
                            except ValueError:
                                continue
                    if timestamp_dt:
                        has_seconds = value.count(':') >= 2
                        if lang_prefix == 'en':
                            return timestamp_dt.strftime('%m/%d/%y %I:%M:%S %p' if has_seconds else '%m/%d/%y %I:%M %p')
                        return timestamp_dt.strftime('%d/%m/%Y %H:%M:%S' if has_seconds else '%d/%m/%Y %H:%M')
                except Exception:
                    pass
                return value

            # Função para obter informações do sistema da licença ativa
            def _get_system_info():
                """Obtém informações do sistema da licença ativa"""
                date_format = '%d/%m/%Y %H:%M:%S' if lang_prefix == 'pt' else '%m/%d/%Y %I:%M:%S %p'
                try:
                    if hasattr(self, 'app_shell') and self.app_shell and hasattr(self.app_shell, 'license_manager'):
                        system_info = self.app_shell.license_manager.get_active_license_system_info(self.com_port)
                        system_info = system_info or {}
                        system_info.setdefault('generated_at', datetime.now().strftime(date_format))
                        return system_info

                    try:
                        from .license_module import LicenseManager
                    except ImportError:
                        from license_module import LicenseManager
                    import os
                    LICENSE_DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "licenses.json")
                    license_manager = LicenseManager(LICENSE_DB_FILE)
                    system_info = license_manager.get_active_license_system_info(self.com_port)
                    system_info = system_info or {}
                    system_info.setdefault('generated_at', datetime.now().strftime(date_format))
                    return system_info
                except Exception as e:
                    print(f"⚠️ Erro geral ao obter informações do sistema: {e}")
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime(date_format)
                    }

            def _display_name(test: dict, fallback_index: int) -> str:
                name = test.get('test_name')
                if name:
                    return name
                fallback_label = self._t('threshold.test', 'Teste')
                return f"{fallback_label} {test.get('id', fallback_index)}"

            import tempfile
            import base64

            doc = SimpleDocTemplate(
                filepath,
                pagesize=A4,
                rightMargin=72,
                leftMargin=72,
                topMargin=72,
                bottomMargin=18
            )

            styles = getSampleStyleSheet()
            title_style = ParagraphStyle(
                'CustomTitle',
                parent=styles['Heading1'],
                fontSize=24,
                spaceAfter=30,
                alignment=TA_CENTER,
                textColor=colors.HexColor('#2c3e50')
            )

            heading_style = ParagraphStyle(
                'CustomHeading',
                parent=styles['Heading2'],
                fontSize=16,
                spaceAfter=12,
                textColor=colors.HexColor('#2c3e50')
            )
            footer_style = ParagraphStyle(
                'FooterStyle',
                parent=styles['Normal'],
                fontSize=10,
                alignment=TA_CENTER,
                textColor=colors.grey
            )

            story = []

            try:
                root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
                logo_path = os.path.join(root, 'assets', 'images', 'fasttag_logo.png')
                if os.path.exists(logo_path):
                    logo_size = 1.5 * inch
                    logo_img = Image(logo_path, width=logo_size, height=logo_size)
                    story.append(logo_img)
                    story.append(Spacer(1, 10))
                    print("Logo Fasttag adicionado ao PDF (proporção circular)")
                else:
                    print("Logo Fasttag não encontrado")
            except Exception as e:
                print(f"Erro ao adicionar logo: {e}")

            story.append(Paragraph(self._t('pdf.report_title_noise', 'Relatório de Testes - Noise Check'), title_style))
            story.append(Spacer(1, 20))

            sysinfo = _get_system_info()
            story.append(Paragraph(self._t('pdf.system_info', 'Informações do Sistema'), heading_style))
            info_text = (
                f"<b>{self._t('pdf.software', 'Software')}</b> {sysinfo.get('software', 'N/A')}<br/>"
                f"<b>{self._t('pdf.hardware', 'Hardware')}</b> {sysinfo.get('hardware', 'N/A')}<br/>"
                f"<b>{self._t('pdf.firmware', 'Firmware')}</b> {sysinfo.get('firmware', 'N/A')}<br/>"
                f"<b>{self._t('pdf.serial_number', 'Serial Number')}</b> {sysinfo.get('serial_number', 'N/A')}<br/>"
                f"<b>{self._t('pdf.license', 'Licença')}</b> {sysinfo.get('license', 'N/A')}<br/>"
                f"<b>{self._t('pdf.generated_at', 'Gerado em:')}</b> {sysinfo.get('generated_at', 'N/A')}"
            )
            story.append(Paragraph(info_text, styles['Normal']))
            story.append(Spacer(1, 20))

            story.append(Paragraph(self._t('pdf.test_description_title_noise', 'Descrição do Teste'), heading_style))
            story.append(Paragraph(self._t(
                'pdf.test_description_text_noise',
                'Este relatório apresenta os resultados das medições de ruído ambiente coletadas pelo módulo Noise Check do FastChecker II.'
            ), styles['Normal']))
            story.append(Spacer(1, 20))

            story.append(Paragraph(self._t('pdf.test_summary_noise', 'Resumo dos Testes'), heading_style))

            table_headers = [
                self._t('pdf.test_name_col', 'Nome'),
                self._t('pdf.duration_col', 'Duração (s)'),
                self._t('pdf.average_col', 'Médio (dBm)'),
                self._t('pdf.min_col', 'Mín (dBm)'),
                self._t('pdf.max_col', 'Máx (dBm)'),
                self._t('pdf.date_time_col', 'Data/Hora'),
                self._t('pdf.severity_col', 'Severidade')
            ]
            table_data = [table_headers]
            test_stats = {}

            for index, test in enumerate(selected_tests):
                display_name = _display_name(test, index + 1)
                duration = test.get('duration', 0)
                timestamp_formatted = _format_datetime(test.get('timestamp'))
                noise_data = test.get('noise_data', {})

                values = []
                if isinstance(noise_data, dict) and noise_data:
                    first_val = next(iter(noise_data.values()))
                    if isinstance(first_val, dict) and 'value' in first_val:
                        values = [v['value'] for v in noise_data.values()]
                    else:
                        values = list(noise_data.values())

                if values:
                    avg_val = sum(values) / len(values)
                    min_val = min(values)
                    max_val = max(values)
                    severity = _calculate_severity(max_val, avg_val)
                else:
                    avg_val = min_val = max_val = 0
                    severity = '-'

                table_data.append([
                    display_name,
                    f"{duration}",
                    f"{avg_val:.1f}",
                    f"{min_val:.1f}",
                    f"{max_val:.1f}",
                    timestamp_formatted,
                    severity
                ])

                test_stats[display_name] = {
                    'duration': duration,
                    'avg': avg_val,
                    'min': min_val,
                    'max': max_val,
                    'severity': severity,
                    'timestamp': timestamp_formatted
                }

            test_table = Table(
                table_data,
                colWidths=[1.8 * inch, 0.9 * inch, 1 * inch, 1 * inch, 1 * inch, 1.4 * inch, 1.0 * inch]
            )
            test_table.setStyle(TableStyle([
                ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#007bff')),
                ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
                ('FONTSIZE', (0, 0), (-1, -1), 8),
                ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
                ('TOPPADDING', (0, 0), (-1, -1), 8),
                ('GRID', (0, 0), (-1, -1), 1, colors.black),
                ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')])
            ]))
            story.append(KeepTogether(test_table))
            story.append(Spacer(1, 20))

            charts = generate_all_charts(selected_tests)
            temp_files = []

            if charts:
                for chart_name, image_base64 in charts:
                    with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
                        tmp_file.write(base64.b64decode(image_base64))
                        tmp_path = tmp_file.name
                        temp_files.append(tmp_path)

                    img = Image(tmp_path, width=7 * inch, height=2.5 * inch)
                    stats = test_stats.get(chart_name, {})
                    chart_info_text = (
                        f"<b>{self._t('pdf.test_name_col', 'Nome')}:</b> {chart_name}<br/>"
                        f"<b>{self._t('pdf.duration_col', 'Duração (s)')}:</b> {stats.get('duration', '-')}<br/>"
                        f"<b>{self._t('pdf.date_time_col', 'Data/Hora')}:</b> {stats.get('timestamp', '-')}<br/>"
                        f"<b>{self._t('pdf.average_col', 'Médio (dBm)')}:</b> {stats.get('avg', 0):.1f} dBm | "
                        f"<b>{self._t('pdf.min_col', 'Mín (dBm)')}:</b> {stats.get('min', 0):.1f} dBm | "
                        f"<b>{self._t('pdf.max_col', 'Máx (dBm)')}:</b> {stats.get('max', 0):.1f} dBm<br/>"
                        f"<b>{self._t('pdf.severity_col', 'Severidade')}:</b> {stats.get('severity', '-')}"
                    )

                    chart_elements = [
                        Paragraph(f"{self._t('pdf.noise_chart_title', 'Noise Check')} - {chart_name}", heading_style),
                        Paragraph(chart_info_text, styles['Normal']),
                        img,
                        Spacer(1, 20)
                    ]
                    story.append(KeepTogether(chart_elements))
            else:
                story.append(Paragraph(self._t('pdf.no_data_available', 'Nenhum dado disponível para este tipo de gráfico'), styles['Normal']))

            story.append(Spacer(1, 20))
            story.append(Paragraph(self._t('pdf.auto_report_footer', 'Relatório automático gerado pelo FastChecker'), footer_style))
            story.append(PageBreak())
            story.append(Spacer(1, 20))
            story.append(Paragraph(self._t('pdf.additional_info', 'Informações Adicionais'), heading_style))
            story.append(Paragraph(self._t('pdf.footer_text', 'Este relatório foi gerado automaticamente pelo FastChecker II.'), styles['Normal']))

            timestamp_format = "%d/%m/%Y às %H:%M:%S" if lang_prefix == 'pt' else "%m/%d/%Y at %I:%M:%S %p"
            story.append(Paragraph(
                f"<b>{self._t('pdf.document_generated_at', 'Documento gerado em:')}</b> {datetime.now().strftime(timestamp_format)}",
                styles['Normal']
            ))

            doc.build(story)

            for temp_file in temp_files:
                try:
                    if os.path.exists(temp_file):
                        os.unlink(temp_file)
                except Exception:
                    pass

            return {'success': True, 'filepath': filepath}

        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_pdf_fallback(self, filepath, selected_tests, html_path=None):
        """Mantido apenas por compatibilidade (não é mais usado)"""
        try:
            from fastchecker.pdf_generator import generate_pdf_only
            return generate_pdf_only(filepath)
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_html_content_for_selected_tests(self, selected_tests):
        """Gera conteúdo HTML apenas com os testes selecionados"""
        try:
            from html_report_generator import _calculate_severity
            from image_chart_generator import generate_all_charts
            
            # Função para obter informações do sistema da licença ativa
            def _get_system_info():
                """Obtém informações do sistema da licença ativa"""
                try:
                    # Tenta obter informações via app_shell (método preferido)
                    if hasattr(self, 'app_shell') and self.app_shell and hasattr(self.app_shell, 'license_manager'):
                        system_info = self.app_shell.license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    
                    # Fallback: cria LicenseManager temporário
                    try:
                        from .license_module import LicenseManager
                        import os
                        LICENSE_DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "licenses.json")
                        license_manager = LicenseManager(LICENSE_DB_FILE)
                        system_info = license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    except Exception as fallback_error:
                        print(f"⚠️ Erro no fallback de informações do sistema: {fallback_error}")
                    
                    # Fallback final: informações básicas
                    from datetime import datetime
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime('%d/%m/%Y %H:%M:%S')
                    }
                    
                except Exception as e:
                    print(f"⚠️ Erro geral ao obter informações do sistema: {e}")
                    from datetime import datetime
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime('%d/%m/%Y %H:%M:%S')
                    }
            
            sysinfo = _get_system_info()
            
            # Logo path
            root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
            logo_path = os.path.join(root, 'assets', 'images', 'fasttag_logo.png')
            logo_exists = os.path.exists(logo_path)
            
            # Logo base64 se existir
            logo_base64 = ""
            if logo_exists:
                import base64
                with open(logo_path, 'rb') as logo_file:
                    logo_base64 = base64.b64encode(logo_file.read()).decode()
            
            # Gera gráficos apenas com os testes selecionados
            charts = generate_all_charts(selected_tests)
            
            # HTML template
            html_content = f"""
            <!DOCTYPE html>
            <html lang="pt-BR">
            <head>
                <meta charset="UTF-8">
                <title>Relatório de Testes Selecionados - Noise Check</title>
                <style>
                    body {{ font-family: Arial, sans-serif; margin: 20px; }}
                    .header {{ text-align: center; margin-bottom: 30px; }}
                    .logo {{ max-width: 200px; height: auto; }}
                    .info {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; }}
                    .test-table {{ width: 100%; border-collapse: collapse; margin-bottom: 30px; }}
                    .test-table th, .test-table td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
                    .test-table th {{ background-color: #f2f2f2; }}
                    .chart {{ text-align: center; margin: 20px 0; }}
                    .chart img {{ max-width: 100%; height: auto; }}
                </style>
            </head>
            <body>
                <div class="header">
                    <h1>Relatório de Testes Selecionados - Noise Check</h1>
                    {f'<img src="data:image/png;base64,{logo_base64}" class="logo" alt="Logo">' if logo_base64 else ''}
                </div>
                
                <div class="info">
                    <h3>Informações do Sistema</h3>
                    <p><strong>Software:</strong> {sysinfo['software']}</p>
                    <p><strong>Hardware:</strong> {sysinfo['hardware']}</p>
                    <p><strong>Firmware:</strong> {sysinfo['firmware']}</p>
                    <p><strong>Serial Number:</strong> {sysinfo['serial_number']}</p>
                    <p><strong>Licença:</strong> {sysinfo['license']}</p>
                    <p><strong>Gerado em:</strong> {sysinfo['generated_at']}</p>
                    <p><strong>Testes incluídos:</strong> {len(selected_tests)}</p>
                </div>
                
                <h3>Resumo dos Testes Selecionados</h3>
                <table class="test-table">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Nome do Teste</th>
                            <th>Duração (s)</th>
                            <th>Ruído Médio (dBm)</th>
                            <th>Ruído Mínimo (dBm)</th>
                            <th>Ruído Máximo (dBm)</th>
                            <th>Hora do Pico</th>
                            <th>Status</th>
                            <th>Data/Hora</th>
                        </tr>
                    </thead>
                    <tbody>
            """
            
            # Adiciona dados dos testes selecionados
            for test in selected_tests:
                noise_data = test.get("noise_data", {})
                if isinstance(noise_data, dict) and noise_data:
                    if isinstance(list(noise_data.values())[0], dict):
                        noise_values = [v["value"] for v in noise_data.values()]
                    else:
                        noise_values = list(noise_data.values())
                    avg_noise = sum(noise_values) / len(noise_values)
                    min_noise = min(noise_values)
                    max_noise = max(noise_values)
                else:
                    avg_noise = min_noise = max_noise = 0.0
                
                max_noise_time = test.get("max_noise_time", "-")
                noise_difference = max_noise - avg_noise
                
                if noise_difference <= 3:
                    status = "Baixa"
                elif 3 < noise_difference <= 6:
                    status = "Média"
                elif 6 < noise_difference <= 9:
                    status = "Alta"
                else:
                    status = "Muito Alta"
                
                html_content += f"""
                        <tr>
                            <td>{test.get('id', 'N/A')}</td>
                            <td>{test.get('test_name', 'Sem nome')}</td>
                            <td>{test.get('duration', 0)}</td>
                            <td>{avg_noise:.1f}</td>
                            <td>{min_noise:.1f}</td>
                            <td>{max_noise:.1f}</td>
                            <td>{max_noise_time}</td>
                            <td>{status}</td>
                            <td>{test.get('timestamp', 'N/A')}</td>
                        </tr>
                """
            
            html_content += """
                    </tbody>
                </table>
            """
            
            # Adiciona gráficos
            for name, image_base64 in charts:
                html_content += f"""
                <div class="chart">
                    <h4>Noise Check - {name}</h4>
                    <img src="data:image/png;base64,{image_base64}" alt="{name}">
                </div>
                """
            
            html_content += """
            </body>
            </html>
            """
            
            return html_content
            
        except Exception as e:
            print(f"❌ Erro ao gerar HTML para testes selecionados: {e}")
            return f"<html><body><h1>Erro ao gerar relatório</h1><p>{str(e)}</p></body></html>"

    def delete_selected_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        selected_items = [item for item in self.history_tree.get_children() if self.history_tree.set(item, 'Plot') == "☑"]
        
        if not selected_items:
            messagebox.showwarning(
                self._t('noise.no_test_selected_delete', "Nenhum Teste Selecionado"),
                self._t('noise.no_test_selected_delete_msg', "Por favor, selecione um ou mais testes no histórico para excluir."),
                parent=self
            )
            return



        deleted_count = 0
        for item in selected_items:
            test_id = self.history_tree.item(item)['values'][1]
            if self.database.delete_test_from_history(test_id):
                deleted_count += 1
        
        messagebox.showinfo(
            self._t('noise.tests_deleted', "Operação Concluída"),
            self._t('noise.tests_deleted_msg', "{count} teste(s) foram excluídos com sucesso.", count=deleted_count),
            parent=self
        )
        self.load_history_to_tree()
        self.update_history_stats()
        self.update_plot_from_history()
    
    def save_test_to_history(self):
        if not DATABASE_AVAILABLE or not self.database or not self.live_data: return
        try:
            # NOVO: Calcula a hora do ruído máximo
            max_noise_time = "-"
            if self.live_data:
                # Encontra o valor máximo de ruído
                if isinstance(list(self.live_data.values())[0], dict):
                    max_noise_value = max(v["value"] for v in self.live_data.values())
                    # Encontra o timestamp correspondente ao valor máximo
                    for time_key, data in self.live_data.items():
                        if data["value"] == max_noise_value:
                            max_noise_time = data["absolute_time"]
                            break
                else:
                    # Para dados antigos, calcula baseado no timestamp relativo
                    max_noise_value = max(self.live_data.values())
                    max_time_key = max(self.live_data.keys(), key=lambda k: self.live_data[k])
                    if self.test_start_time:
                        max_absolute_time = self.test_start_time + timedelta(seconds=float(max_time_key))
                        max_noise_time = max_absolute_time.strftime("%H:%M:%S")
            
            # Calcula duração real do teste (em segundos) com base nos dados coletados
            real_duration = self.get_scan_duration()
            try:
                if self.live_data:
                    first_val = next(iter(self.live_data.values()))
                    # As chaves de self.live_data são tempos relativos em segundos
                    durations = [float(t) for t in self.live_data.keys()]
                    if durations:
                        computed_duration = max(durations)
                        if computed_duration > 0:
                            # Armazena com 1 casa decimal para maior precisão
                            real_duration = round(computed_duration, 1)
            except Exception:
                # Em caso de qualquer problema, mantém a duração configurada
                pass

            test_data = {
                "test_name": self.current_test_name, "duration": real_duration,
                "run_mode": self.run_mode_var.get(), "frequency": FIXED_FREQ_MHZ,
                "noise_data": self.live_data.copy(), "timestamp": datetime.now().strftime("%d-%m-%Y %H:%M"),
                "max_noise_time": max_noise_time, "show_in_graph": True
            }
            if self.database.add_test_to_history(test_data):
                print(f"✅ Teste '{self.current_test_name}' salvo no histórico")
                self.test_history = self.database.get_test_history()
                self.update_history_stats()
                self.load_history_to_tree()
                # CORREÇÃO: Limpa dados ao vivo após salvar no histórico para evitar duplicação no gráfico
                self.live_data.clear()
                self.update_plot_from_history()  # Atualiza o gráfico com apenas os dados do histórico
            else:
                print(f"⚠️ Erro ao salvar teste '{self.current_test_name}' no histórico")
        except Exception as e:
            print(f"❌ Erro ao salvar teste no histórico: {e}")
    
    def get_temperature(self):
        ### ALTERADO: usa self.com_port ###
        if not rfid_sdk or rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) != 0: return None
        try:
            output_buffer, output_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
            status = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_TEMPERATURE, None, 0, output_buffer, ctypes.byref(output_len))
            if status == 0 and output_len.value >= 3: return struct.unpack('>h', output_buffer.raw[1:3])[0] / 100.0
        finally: 
            ### ALTERADO: usa self.com_port ###
            rfid_sdk.UHF_RFID_Close(self.com_port)
        return None

    def update_history_stats(self):
        if not hasattr(self, 'history_stats_label'):
            return
        
        if not DATABASE_AVAILABLE or not self.database:
            stats_text = self._t('noise.stats_db_unavailable', 'Estatísticas: Banco de dados não disponível')
        else:
            try:
                stats = self.database.get_statistics()
                count = stats.get('total_tests', 0)
                duration = stats.get('total_duration', 0.0)
                avg_noise = stats.get('avg_noise_level', 0.0)
                key = 'noise.stats_format_single' if count == 1 else 'noise.stats_format'
                default = "Estatísticas: {count} testes | Duração total: {duration:.1f}s | Ruído médio: {avg_noise:.1f} dBm"
                stats_text = self._t(key, default, count=count, duration=duration, avg_noise=avg_noise)
            except Exception as e:
                stats_text = self._t('noise.stats_load_error', f"Estatísticas: Erro ao carregar ({e})", error=e)
        
        self.history_stats_label.config(text=stats_text)

    def show_severity_help(self):
        """Mostra popup com tabela de definição de severidade"""
        help_window = tk.Toplevel(self)
        help_window.title("Definição de Severidade do Ruído")
        help_window.geometry("650x450")
        help_window.resizable(False, False)
        help_window.transient(self)
        help_window.grab_set()
        
        # Centraliza a janela
        help_window.update_idletasks()
        x = (help_window.winfo_screenwidth() // 2) - (650 // 2)
        y = (help_window.winfo_screenheight() // 2) - (450 // 2)
        help_window.geometry(f"650x450+{x}+{y}")
        
        # Frame principal
        main_frame = ttk.Frame(help_window, padding="20")
        main_frame.pack(fill="both", expand=True)
        
        # Título
        title_label = ttk.Label(main_frame, text="Critério de Classificação de Severidade", 
                               font=("Segoe UI", 14))
        title_label.pack(pady=(0, 20))
        
        # Explicação simplificada
        explanation_text = """Severidade = Ruído Máximo - Ruído Médio"""
        
        explanation_label = ttk.Label(main_frame, text=explanation_text, 
                                    font=("Segoe UI", 10), justify="left")
        explanation_label.pack(pady=(0, 20))
        
        # Frame para a tabela com Canvas para dots coloridos
        table_frame = ttk.Frame(main_frame)
        table_frame.pack(fill="both", expand=True)
        
        # Cria um Canvas para desenhar os dots coloridos
        canvas = tk.Canvas(table_frame, height=200, bg="white")
        canvas.pack(fill="both", expand=True)
        
        # Dados de severidade com cores
        severity_data = [
            ("≤ 3 dBm", "Baixa", "Ruído estável, pouca variação - Ambiente ideal", "#00AA00"),
            ("3 < x ≤ 6 dBm", "Média", "Variação moderada de ruído - Aceitável", "#FFD700"),
            ("6 < x ≤ 9 dBm", "Alta", "Variação significativa de ruído - Pode afetar performance", "#FF8C00"),
            ("> 9 dBm", "Muito Alta", "Variação extrema de ruído - Operação comprometida", "#FF0000")
        ]
        
        # Desenha os dots coloridos e texto
        y_start = 20
        for i, (diff, severity, desc, color) in enumerate(severity_data):
            y_pos = y_start + (i * 45)
            
            # Desenha o dot colorido (círculo) - 50% menor (10x10 em vez de 20x20)
            canvas.create_oval(20, y_pos + 5, 30, y_pos + 15, fill=color, outline="black", width=1)
            
            # Adiciona o texto da severidade (sem negrito)
            canvas.create_text(40, y_pos + 10, text=severity, anchor="w", font=("Segoe UI", 10))
            
            # Adiciona o texto da diferença
            canvas.create_text(150, y_pos + 10, text=diff, anchor="w", font=("Segoe UI", 9))
            
            # Adiciona a descrição
            canvas.create_text(300, y_pos + 10, text=desc, anchor="w", font=("Segoe UI", 9))
        
        # Frame para botões
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill="x", pady=(20, 0))
        
        # Botão Fechar
        close_button = ttk.Button(button_frame, text="Fechar", command=help_window.destroy)
        close_button.pack(side="right")
        
        # Informação adicional
        info_text = """• Ruído Médio: Média aritmética de todos os valores coletados
• Ruído Máximo: Maior valor registrado durante o teste
• A classificação é aplicada automaticamente aos testes"""
        
        info_label = ttk.Label(main_frame, text=info_text, 
                              font=("Segoe UI", 9), justify="left", 
                              foreground="gray")
        info_label.pack(pady=(10, 0))
    
    def load_history_to_tree(self, tree=None):
        if tree is None: tree = self.history_tree
        for item in tree.get_children(): tree.delete(item)
        if not DATABASE_AVAILABLE or not self.database: return
        try:
            self.test_history = self.database.get_test_history()
            for test in self.test_history:
                noise_data = test.get("noise_data", {})
                if isinstance(noise_data, dict) and noise_data:
                    # NOVO: Extrai valores da nova estrutura de dados
                    if isinstance(list(noise_data.values())[0], dict):
                        noise_values = [v["value"] for v in noise_data.values()]
                    else:
                        noise_values = list(noise_data.values())
                    avg_noise = sum(noise_values) / len(noise_values)
                    min_noise = min(noise_values); max_noise = max(noise_values)
                else: avg_noise = 0.0; min_noise = 0.0; max_noise = 0.0
                
                # NOVO: Obtém a hora do ruído máximo
                max_noise_time = test.get("max_noise_time", "-")
                
                # NOVO: Calcula a diferença entre ruído máximo e médio para classificação
                noise_difference = max_noise - avg_noise
                
                # NOVO: Critérios baseados na diferença ruído máximo - ruído médio
                if noise_difference <= 3:
                    status = "Baixa"
                elif 3 < noise_difference <= 6:
                    status = "Média"
                elif 6 < noise_difference <= 9:
                    status = "Alta"
                else:  # noise_difference > 9
                    status = "Muito Alta"
                
                plot_state = "☑" if test.get('show_in_graph', False) else "☐"
                
                tree.insert("", "end", values=(
                    plot_state, test.get("id", ""), test.get("test_name", "Sem nome"),
                    test.get("duration", 0), f"{avg_noise:.1f}", f"{min_noise:.1f}", f"{max_noise:.1f}",
                    max_noise_time, test.get("timestamp", ""), status
                ))
            
            # NOVO: Interface light sem cores nas linhas
            # Atualiza o estado do botão de relatório após carregar o histórico
            self.update_report_button_state()
            
            # Scroll automático para o último teste (mais recente)
            if tree.get_children():
                last_item = tree.get_children()[-1]  # Pega o último item
                tree.see(last_item)  # Faz scroll para o último item
                tree.selection_set(last_item)  # Seleciona o último item
        except Exception as e:
            messagebox.showerror("Erro", f"Erro ao carregar histórico: {e}", parent=self)

    def on_history_tree_click(self, event):
        """Manipula cliques na árvore de histórico - detecta clique simples e duplo clique"""
        self._click_count += 1
        
        if self._click_count == 1:
            # Primeiro clique - inicia timer
            self._click_timer = self.after(300, self._handle_single_click, event)
        elif self._click_count == 2:
            # Segundo clique - cancela timer e trata como duplo clique
            if self._click_timer:
                self.after_cancel(self._click_timer)
            self._click_timer = None
            self._click_count = 0
            self._handle_double_click(event)
    
    def _handle_single_click(self, event):
        """Trata clique simples - toggle do checkbox Plot"""
        self._click_count = 0
        region = self.history_tree.identify("region", event.x, event.y)
        # Só aceita cliques na coluna do checkbox (coluna 1)
        if region == "cell":
            column_id = self.history_tree.identify_column(event.x)
            # Coluna 1 é o checkbox (Plot)
            if column_id == '#1':
                item = self.history_tree.identify_row(event.y)
                if item:
                    test_id = self.history_tree.item(item)['values'][1]
                    if DATABASE_AVAILABLE and self.database:
                        test_data = self.database.get_test_by_id(test_id)
                        if test_data:
                            current_state = self.history_tree.set(item, 'Plot')
                            new_state = "☑" if current_state == "☐" else "☐"
                            self.history_tree.set(item, 'Plot', new_state)
                        test_data['show_in_graph'] = (new_state == "☑")
                        self.database.update_test_in_history(test_id, test_data)
                        self.update_plot_from_history()
                        # Atualiza o estado do botão de relatório
                        self.update_report_button_state()
    
    def _handle_double_click(self, event):
        """Trata duplo clique - edição do nome do teste"""
        # Destrói qualquer widget de edição que já exista
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()

        region = self.history_tree.identify("region", event.x, event.y)
        if region != "cell":
            return

        column_id = self.history_tree.identify_column(event.x)
        # As colunas de exibição são ("Plot", "Nome", ...), então "Nome" é a coluna #2
        if column_id != '#2':
            return
            
        item_id = self.history_tree.identify_row(event.y)
        if not item_id:
            return

        # Obtém as coordenadas da célula para posicionar o widget de edição
        x, y, width, height = self.history_tree.bbox(item_id, column_id)

        # Cria e posiciona o widget de edição
        current_name = self.history_tree.set(item_id, 'Nome')
        self._edit_var = tk.StringVar(value=current_name)
        # --- LINHA CORRIGIDA ---
        self._edit_entry = ttk.Entry(self.history_tree, textvariable=self._edit_var)
        self._edit_entry.place(x=x, y=y, width=width, height=height)
        self._edit_entry.focus_set()
        self._edit_entry.selection_range(0, 'end')

        # Associa eventos para salvar ou cancelar a edição
        self._edit_entry.bind("<Return>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<FocusOut>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<Escape>", lambda e: self.cancel_edit_name())
    
    # --- MÉTODOS PARA EDIÇÃO IN-LINE ---

    def save_edited_name(self, item_id):
        """Salva o nome do teste que foi editado."""
        if not (hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists()):
            return
            
        try:
            new_name = self._edit_var.get().strip()
            self.cancel_edit_name() # Destrói o widget de edição

            if not new_name:
                self._set_status('noise.status_empty_name_error', "Erro: O nome do teste não pode ser vazio.")
                return

            # Obtém o ID do teste a partir dos valores da linha
            test_id = self.history_tree.item(item_id)['values'][1]

            if DATABASE_AVAILABLE and self.database:
                # NOVO: Verifica se o novo nome já existe (excluindo o teste atual)
                if self.check_duplicate_test_name_excluding_current(new_name, test_id):
                    messagebox.showerror(
                        self._t('noise.name_duplicate_inline', "Nome Duplicado"),
                        self._t('noise.name_duplicate_inline_msg', "Já existe um teste com o nome '{name}' no histórico.\n\nPor favor, escolha um nome diferente.", name=new_name),
                        parent=self
                    )
                    return
                
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    test_data['test_name'] = new_name
                    if self.database.update_test_in_history(test_id, test_data):
                        self.history_tree.set(item_id, 'Nome', new_name)
                        self._set_status('noise.status_name_updated', "Nome do teste ID {test_id} atualizado.", test_id=test_id)
                        print(f"✅ Nome do teste ID {test_id} atualizado para '{new_name}'")
                        # NOVO: Atualiza o gráfico imediatamente após editar o nome
                        self.update_plot_from_history()
                    else:
                        self._set_status('noise.status_db_update_error', "Erro ao atualizar o teste no banco de dados.")
                else:
                    self._set_status('noise.status_test_not_found', "Erro: Teste com ID {test_id} não encontrado.", test_id=test_id)
        except Exception as e:
            print(f"❌ Erro ao salvar nome editado: {e}")
            self._set_status('noise.status_save_error', "Erro ao salvar: {error}", error=e)

    def cancel_edit_name(self, event=None):
        """Cancela o processo de edição e destrói o widget."""
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()
            delattr(self, '_edit_entry')

    # --- FIM DOS MÉTODOS DE EDIÇÃO ---

    def update_plot_from_history(self):
        if not DATABASE_AVAILABLE or not self.database: return
        try:
            self.multiple_tests.clear()
            selected_tests = []
            for item in self.history_tree.get_children():
                if self.history_tree.set(item, 'Plot') == "☑":
                    test_id = self.history_tree.item(item)['values'][1]
                    test_data = self.database.get_test_by_id(test_id)
                    if test_data: selected_tests.append(test_data)
            
            for test in selected_tests:
                test_name = test.get('test_name', f'Teste_{test.get("id")}')
                test_id = test.get('id')
                noise_data = test.get('noise_data', {})
                if isinstance(noise_data, dict) and noise_data:
                    # NOVO: Converte dados para formato compatível com gráfico e inclui ID
                    if isinstance(list(noise_data.values())[0], dict):
                        converted_data = {float(t): v["value"] for t, v in noise_data.items()}
                    else:
                        converted_data = {float(t): v for t, v in noise_data.items()}
                    
                    # NOVO: Armazena dados com ID para uso nas cores
                    self.multiple_tests[test_name] = {
                        'data': converted_data,
                        'id': test_id
                    }
            
            self.update_plot()
            if selected_tests:
                self._set_status('noise.status_plotting_tests', "Plotando {count} teste(s) do histórico", count=len(selected_tests))
            else:
                self._set_status('noise.status_no_tests_to_plot', "Nenhum teste selecionado para plotagem")
        except Exception as e:
            self._set_status('noise.status_plot_error', "Erro ao atualizar gráfico: {error}", error=e)

    def select_all_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and not test_data.get('show_in_graph', False):
                self.history_tree.set(item, 'Plot', '☑')
                test_data['show_in_graph'] = True
                self.database.update_test_in_history(test_id, test_data)
        self.update_plot_from_history()
        # Atualiza o estado do botão de relatório
        self.update_report_button_state()

    def deselect_all_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and test_data.get('show_in_graph', False):
                self.history_tree.set(item, 'Plot', '☐')
                test_data['show_in_graph'] = False
                self.database.update_test_in_history(test_id, test_data)
        self.update_plot_from_history()
        # Atualiza o estado do botão de relatório
        self.update_report_button_state()

    def sort_treeview(self, column):
        """
        Ordena a tabela de histórico pela coluna especificada
        """
        try:
            # Obtém todos os itens da tabela
            items = [(self.history_tree.set(item, column), item) for item in self.history_tree.get_children('')]
            
            # Verifica se é a mesma coluna para alternar direção
            if self.current_sort_column == column:
                self.current_sort_reverse = not self.current_sort_reverse
            else:
                self.current_sort_column = column
                self.current_sort_reverse = False
            
            # Determina o tipo de ordenação baseado na coluna
            if column in ["Duração", "ID"]:
                # Ordenação numérica
                items.sort(key=lambda x: int(x[0]) if x[0].isdigit() else 0, reverse=self.current_sort_reverse)
            elif column in ["Ruído Médio", "Ruído Mínimo", "Ruído Máximo"]:
                # Ordenação numérica decimal
                items.sort(key=lambda x: float(x[0].replace(" dBm", "")) if x[0] != "" else 0, reverse=self.current_sort_reverse)
            elif column == "Hora Ruído Máximo":
                # Ordenação de horário
                def time_key(x):
                    if x[0] == "-" or x[0] == "":
                        return "00:00:00"
                    return x[0]
                items.sort(key=time_key, reverse=self.current_sort_reverse)
            elif column == "Data/Hora":
                # Ordenação de data/hora
                items.sort(key=lambda x: x[0], reverse=self.current_sort_reverse)
            elif column == "Plot":
                # Ordenação por estado (☐ primeiro, depois ☑)
                def plot_key(x):
                    return 0 if x[0] == "☐" else 1
                items.sort(key=plot_key, reverse=self.current_sort_reverse)
            elif column == "Severidade":
                # NOVO: Ordenação hierárquica por severidade
                def severity_key(x):
                    severity_order = {"Baixa": 1, "Média": 2, "Alta": 3, "Muito Alta": 4}
                    return severity_order.get(x[0], 0)
                items.sort(key=severity_key, reverse=self.current_sort_reverse)
            else:
                # Ordenação alfabética para outras colunas
                items.sort(key=lambda x: x[0].lower(), reverse=self.current_sort_reverse)
            
            # Reorganiza os itens na tabela
            for index, (val, item) in enumerate(items):
                self.history_tree.move(item, '', index)
            
            # Atualiza o símbolo de ordenação no cabeçalho
            self.update_sort_indicator(column)
            
        except Exception as e:
            print(f"Erro na ordenação: {e}")
    
    def update_sort_indicator(self, column):
        """
        Atualiza os símbolos de ordenação nos cabeçalhos
        """
        try:
            # Remove símbolos de todas as colunas
            for col in ["Plot", "ID", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade"]:
                current_text = self.history_tree.heading(col)['text']
                # Remove símbolos existentes
                if " ↑" in current_text:
                    current_text = current_text.replace(" ↑", "")
                elif " ↓" in current_text:
                    current_text = current_text.replace(" ↓", "")
                # Adiciona símbolo neutro
                if not current_text.endswith(" ↕"):
                    current_text += " ↕"
                self.history_tree.heading(col, text=current_text)
            
            # Adiciona símbolo de direção na coluna atual
            current_text = self.history_tree.heading(column)['text']
            current_text = current_text.replace(" ↕", "")
            if self.current_sort_reverse:
                current_text += " ↓"
            else:
                current_text += " ↑"
            self.history_tree.heading(column, text=current_text)
            
        except Exception as e:
            print(f"Erro ao atualizar indicador de ordenação: {e}")

    def auto_plot_selected_tests(self):
        self.update_plot_from_history()

    def _setup_zoom_controls(self, parent):
        """Configura os controles de zoom na parte superior esquerda do gráfico"""
        # Frame para os controles de zoom
        zoom_frame = ttk.Frame(parent)
        zoom_frame.grid(row=0, column=0, sticky="nw", padx=5, pady=5)
        
        # Botão Zoom In
        zoom_in_btn = tk.Button(zoom_frame, text="🔍+", width=3, height=1, 
                               command=self._zoom_in, font=("Arial", 10))
        zoom_in_btn.grid(row=0, column=0, padx=2)
        
        # Botão Zoom Out  
        zoom_out_btn = tk.Button(zoom_frame, text="🔍-", width=3, height=1,
                                command=self._zoom_out, font=("Arial", 10))
        zoom_out_btn.grid(row=0, column=1, padx=2)
        
        # Botão Reset Zoom
        reset_btn = tk.Button(zoom_frame, text="↻", width=3, height=1,
                             command=self._reset_zoom, font=("Arial", 10))
        reset_btn.grid(row=0, column=2, padx=2)
        
        # Botão Fit to Data
        fit_btn = tk.Button(zoom_frame, text="📐", width=3, height=1,
                           command=self._fit_to_data, font=("Arial", 10))
        fit_btn.grid(row=0, column=3, padx=2)
        
        # Botão Pan (arrastar)
        self.pan_active = False
        self.pan_btn = tk.Button(zoom_frame, text="✋", width=3, height=1,
                                 command=self._toggle_pan, font=("Arial", 10),
                                 relief=tk.RAISED)
        self.pan_btn.grid(row=0, column=4, padx=2)
        
        # Label do fator de zoom
        self.zoom_label = tk.Label(zoom_frame, text="Zoom: 1.0x", font=("Arial", 9))
        self.zoom_label.grid(row=0, column=5, padx=5)

    def _zoom_in(self):
        """Aplica zoom in no gráfico"""
        try:
            # Obtém limites atuais
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            
            # Calcula centro dos limites atuais
            x_center = (xlim[0] + xlim[1]) / 2
            y_center = (ylim[0] + ylim[1]) / 2
            
            # Calcula nova amplitude (zoom in = amplitude menor)
            x_range = xlim[1] - xlim[0]
            y_range = ylim[1] - ylim[0]
            zoom_factor = 0.8  # Reduz amplitude em 20%
            
            new_x_range = x_range * zoom_factor
            new_y_range = y_range * zoom_factor
            
            # Define novos limites
            new_xlim = (x_center - new_x_range/2, x_center + new_x_range/2)
            new_ylim = (y_center - new_y_range/2, y_center + new_y_range/2)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw()
            
            # Atualiza fator de zoom
            self.zoom_factor *= 1.25
            self.zoom_label.config(text=f"Zoom: {self.zoom_factor:.1f}x")
            
            print("✅ Zoom in aplicado no Noise Check")
            
        except Exception as e:
            print(f"❌ Erro ao aplicar zoom in: {e}")

    def _zoom_out(self):
        """Aplica zoom out no gráfico"""
        try:
            # Obtém limites atuais
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            
            # Calcula centro dos limites atuais
            x_center = (xlim[0] + xlim[1]) / 2
            y_center = (ylim[0] + ylim[1]) / 2
            
            # Calcula nova amplitude (zoom out = amplitude maior)
            x_range = xlim[1] - xlim[0]
            y_range = ylim[1] - ylim[0]
            zoom_factor = 1.25  # Aumenta amplitude em 25%
            
            new_x_range = x_range * zoom_factor
            new_y_range = y_range * zoom_factor
            
            # Define novos limites
            new_xlim = (x_center - new_x_range/2, x_center + new_x_range/2)
            new_ylim = (y_center - new_y_range/2, y_center + new_y_range/2)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw()
            
            # Atualiza fator de zoom
            self.zoom_factor *= 0.8
            self.zoom_label.config(text=f"Zoom: {self.zoom_factor:.1f}x")
            
            print("✅ Zoom out aplicado no Noise Check")
            
        except Exception as e:
            print(f"❌ Erro ao aplicar zoom out: {e}")

    def _reset_zoom(self):
        """Reseta o zoom para os limites originais"""
        try:
            if self.original_xlim and self.original_ylim:
                self.ax.set_xlim(self.original_xlim)
                self.ax.set_ylim(self.original_ylim)
            else:
                # Se não há limites originais salvos, usa a escala dinâmica
                x_min, x_max = self.get_dynamic_x_scale()
                self.ax.set_xlim(x_min, x_max)
                self.ax.set_ylim(-80, -40)
            
            self.canvas.draw()
            
            # Reseta fator de zoom
            self.zoom_factor = 1.0
            self.zoom_label.config(text="Zoom: 1.0x")
            
            print("✅ Zoom resetado no Noise Check")
            
        except Exception as e:
            print(f"❌ Erro ao resetar zoom: {e}")

    def _fit_to_data(self):
        """Ajusta a visualização automaticamente aos dados"""
        try:
            # Obtém todos os dados plotados
            lines = self.ax.get_lines()
            if not lines:
                return
                
            # Encontra limites dos dados (excluindo linhas de limite)
            x_data = []
            y_data = []
            
            for line in lines:
                # Pula linhas de limite (linhas horizontais ou com poucos pontos)
                x_line = line.get_xdata()
                y_line = line.get_ydata()
                
                # Debug: mostra informações de cada linha
                print(f"🔍 Linha encontrada: {len(x_line)} pontos, X: {min(x_line) if len(x_line) > 0 else 'N/A'}-{max(x_line) if len(x_line) > 0 else 'N/A'} s")
                
                # Filtra apenas linhas com dados de ruído (mais de 2 pontos e range X > 1 s)
                if len(x_line) > 2:
                    x_range_line = max(x_line) - min(x_line)
                    if x_range_line > 1:  # Linha de dados de ruído
                        x_data.extend(x_line)
                        y_data.extend(y_line)
                        print(f"✅ Linha de dados incluída: {len(x_line)} pontos, range X: {x_range_line:.1f} s")
            
            if not x_data or not y_data:
                print("❌ Nenhum dado de ruído encontrado")
                return
                
            # Calcula limites com margem
            x_min, x_max = min(x_data), max(x_data)
            y_min, y_max = min(y_data), max(y_data)
            
            print(f"🔍 Dados filtrados: X({x_min:.1f}-{x_max:.1f} s), Y({y_min:.1f}-{y_max:.1f} dBm)")
            
            # Margem maior para melhor visualização
            x_range = x_max - x_min
            y_range = y_max - y_min
            
            # Se o range for muito pequeno, usa margem absoluta menor
            if x_range < 10:  # Menos de 10 segundos de range
                x_margin = 1  # 1 segundo de margem para ranges pequenos
            else:
                x_margin = x_range * 0.05  # 5% de margem para ranges maiores
                
            if y_range < 5:  # Menos de 5 dBm de range
                y_margin = 1  # 1 dBm de margem
            else:
                y_margin = y_range * 0.05  # 5% de margem
            
            # Define novos limites
            new_x_min = max(0, x_min - x_margin)  # Não vai abaixo de 0
            new_x_max = x_max + x_margin
            new_y_min = y_min - y_margin
            new_y_max = y_max + y_margin
            
            self.ax.set_xlim(new_x_min, new_x_max)
            self.ax.set_ylim(new_y_min, new_y_max)
            self.canvas.draw()
            
            print(f"✅ Visualização ajustada aos dados no Noise Check: X({new_x_min:.1f}-{new_x_max:.1f}s), Y({new_y_min:.1f}-{new_y_max:.1f}dBm)")
            
        except Exception as e:
            print(f"❌ Erro ao ajustar visualização: {e}")

    def _toggle_pan(self):
        """Ativa/desativa o modo pan (arrastar gráfico)"""
        self.pan_active = not self.pan_active
        
        if self.pan_active:
            # Ativa modo pan
            self.pan_btn.config(relief=tk.SUNKEN, bg='#d0d0d0')
            self.canvas.mpl_connect('button_press_event', self._on_pan_press)
            self.canvas.mpl_connect('button_release_event', self._on_pan_release)
            self.canvas.mpl_connect('motion_notify_event', self._on_pan_motion)
            self.pan_start = None
            print("✅ Modo Pan ativado - clique e arraste para mover o gráfico")
        else:
            # Desativa modo pan
            self.pan_btn.config(relief=tk.RAISED, bg='SystemButtonFace')
            # Desconecta eventos (matplotlib mantém conexões internamente)
            print("✅ Modo Pan desativado")

    def _on_pan_press(self, event):
        """Callback quando pressiona o mouse no modo pan"""
        if self.pan_active and event.inaxes == self.ax:
            self.pan_start = (event.xdata, event.ydata)
            self.pan_xlim = self.ax.get_xlim()
            self.pan_ylim = self.ax.get_ylim()

    def _on_pan_release(self, event):
        """Callback quando solta o mouse no modo pan"""
        if self.pan_active:
            self.pan_start = None

    def _on_pan_motion(self, event):
        """Callback quando move o mouse no modo pan"""
        if self.pan_active and self.pan_start and event.inaxes == self.ax:
            # Calcula deslocamento
            dx = self.pan_start[0] - event.xdata
            dy = self.pan_start[1] - event.ydata
            
            # Aplica deslocamento aos limites
            new_xlim = (self.pan_xlim[0] + dx, self.pan_xlim[1] + dx)
            new_ylim = (self.pan_ylim[0] + dy, self.pan_ylim[1] + dy)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw_idle()

if __name__ == '__main__':
    root = tk.Tk()
    root.title("Teste Isolado - Módulo de Ruído")
    root.geometry("1100x750")
    
    from port_manager import get_com_port_number
    com_port = get_com_port_number()
    if com_port is None:
        # CORREÇÃO: Não exibe erro de hardware quando sem licença (modo browser)
        print("ℹ️ Modo browser - hardware não encontrado")
        # Cria uma instância com porta padrão para modo browser
        app = NoiseModule(root, is_licensed=False, com_port=4)  # COM4 como padrão
        app.pack(fill="both", expand=True)
        root.mainloop()
    else:
        app = NoiseModule(root, is_licensed=True, com_port=com_port)
        app.pack(fill="both", expand=True)
        root.mainloop()